From d3ffe336de677e7dafff03253aa6c9adc49cf326 Mon Sep 17 00:00:00 2001 From: Ali Abbas Rizvi Date: Wed, 13 Nov 2019 17:40:21 -0800 Subject: [PATCH] chore: Formatting code (#222) --- .flake8 | 5 + optimizely/bucketer.py | 136 +- optimizely/config_manager.py | 99 +- optimizely/decision_service.py | 567 +- optimizely/entities.py | 158 +- optimizely/error_handler.py | 18 +- optimizely/event/event_factory.py | 182 +- optimizely/event/event_processor.py | 392 +- optimizely/event/log_event.py | 16 +- optimizely/event/payload.py | 134 +- optimizely/event/user_event.py | 66 +- optimizely/event/user_event_factory.py | 74 +- optimizely/event_builder.py | 294 +- optimizely/event_dispatcher.py | 25 +- optimizely/exceptions.py | 45 +- optimizely/helpers/audience.py | 72 +- optimizely/helpers/condition.py | 396 +- .../helpers/condition_tree_evaluator.py | 70 +- optimizely/helpers/constants.py | 391 +- optimizely/helpers/enums.py | 155 +- optimizely/helpers/event_tag_utils.py | 147 +- optimizely/helpers/experiment.py | 4 +- optimizely/helpers/validator.py | 164 +- optimizely/lib/pymmh3.py | 493 +- optimizely/logger.py | 96 +- optimizely/notification_center.py | 123 +- optimizely/optimizely.py | 925 +- optimizely/project_config.py | 545 +- optimizely/user_profile.py | 48 +- setup.py | 48 +- tests/base.py | 1841 ++-- tests/benchmarking/benchmarking_tests.py | 358 +- tests/benchmarking/data.py | 4899 ++++------- tests/helpers_tests/test_audience.py | 483 +- tests/helpers_tests/test_condition.py | 2118 ++--- .../test_condition_tree_evaluator.py | 242 +- tests/helpers_tests/test_event_tag_utils.py | 237 +- tests/helpers_tests/test_experiment.py | 32 +- tests/helpers_tests/test_validator.py | 411 +- tests/test_bucketing.py | 653 +- tests/test_config.py | 2221 +++-- tests/test_config_manager.py | 183 +- tests/test_decision_service.py | 2165 +++-- tests/test_event_builder.py | 1511 ++-- tests/test_event_dispatcher.py | 116 +- tests/test_event_factory.py | 1529 ++-- tests/test_event_payload.py | 183 +- tests/test_event_processor.py | 795 +- tests/test_logger.py | 227 +- tests/test_notification_center.py | 141 +- tests/test_optimizely.py | 7718 +++++++++-------- tests/test_user_event_factory.py | 221 +- tests/test_user_profile.py | 64 +- tests/testapp/application.py | 499 +- tests/testapp/user_profile_service.py | 22 +- tox.ini | 9 - 56 files changed, 16854 insertions(+), 17912 deletions(-) create mode 100644 .flake8 delete mode 100644 tox.ini diff --git a/.flake8 b/.flake8 new file mode 100644 index 00000000..f31217bf --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +# E722 - do not use bare 'except' +ignore = E722 +exclude = optimizely/lib/pymmh3.py,*virtualenv* +max-line-length = 120 diff --git a/optimizely/bucketer.py b/optimizely/bucketer.py index 24a23ef9..1cf71b85 100644 --- a/optimizely/bucketer.py +++ b/optimizely/bucketer.py @@ -12,10 +12,11 @@ # limitations under the License. import math + try: - import mmh3 + import mmh3 except ImportError: - from .lib import pymmh3 as mmh3 + from .lib import pymmh3 as mmh3 MAX_TRAFFIC_VALUE = 10000 @@ -27,15 +28,15 @@ class Bucketer(object): - """ Optimizely bucketing algorithm that evenly distributes visitors. """ + """ Optimizely bucketing algorithm that evenly distributes visitors. """ - def __init__(self): - """ Bucketer init method to set bucketing seed and logger instance. """ + def __init__(self): + """ Bucketer init method to set bucketing seed and logger instance. """ - self.bucket_seed = HASH_SEED + self.bucket_seed = HASH_SEED - def _generate_unsigned_hash_code_32_bit(self, bucketing_id): - """ Helper method to retrieve hash code. + def _generate_unsigned_hash_code_32_bit(self, bucketing_id): + """ Helper method to retrieve hash code. Args: bucketing_id: ID for bucketing. @@ -44,11 +45,11 @@ def _generate_unsigned_hash_code_32_bit(self, bucketing_id): Hash code which is a 32 bit unsigned integer. """ - # Adjusting MurmurHash code to be unsigned - return (mmh3.hash(bucketing_id, self.bucket_seed) & UNSIGNED_MAX_32_BIT_VALUE) + # Adjusting MurmurHash code to be unsigned + return mmh3.hash(bucketing_id, self.bucket_seed) & UNSIGNED_MAX_32_BIT_VALUE - def _generate_bucket_value(self, bucketing_id): - """ Helper function to generate bucket value in half-closed interval [0, MAX_TRAFFIC_VALUE). + def _generate_bucket_value(self, bucketing_id): + """ Helper function to generate bucket value in half-closed interval [0, MAX_TRAFFIC_VALUE). Args: bucketing_id: ID for bucketing. @@ -57,11 +58,11 @@ def _generate_bucket_value(self, bucketing_id): Bucket value corresponding to the provided bucketing ID. """ - ratio = float(self._generate_unsigned_hash_code_32_bit(bucketing_id)) / MAX_HASH_VALUE - return math.floor(ratio * MAX_TRAFFIC_VALUE) + ratio = float(self._generate_unsigned_hash_code_32_bit(bucketing_id)) / MAX_HASH_VALUE + return math.floor(ratio * MAX_TRAFFIC_VALUE) - def find_bucket(self, project_config, bucketing_id, parent_id, traffic_allocations): - """ Determine entity based on bucket value and traffic allocations. + def find_bucket(self, project_config, bucketing_id, parent_id, traffic_allocations): + """ Determine entity based on bucket value and traffic allocations. Args: project_config: Instance of ProjectConfig. @@ -73,22 +74,21 @@ def find_bucket(self, project_config, bucketing_id, parent_id, traffic_allocatio Entity ID which may represent experiment or variation. """ - bucketing_key = BUCKETING_ID_TEMPLATE.format(bucketing_id=bucketing_id, parent_id=parent_id) - bucketing_number = self._generate_bucket_value(bucketing_key) - project_config.logger.debug('Assigned bucket %s to user with bucketing ID "%s".' % ( - bucketing_number, - bucketing_id - )) + bucketing_key = BUCKETING_ID_TEMPLATE.format(bucketing_id=bucketing_id, parent_id=parent_id) + bucketing_number = self._generate_bucket_value(bucketing_key) + project_config.logger.debug( + 'Assigned bucket %s to user with bucketing ID "%s".' % (bucketing_number, bucketing_id) + ) - for traffic_allocation in traffic_allocations: - current_end_of_range = traffic_allocation.get('endOfRange') - if bucketing_number < current_end_of_range: - return traffic_allocation.get('entityId') + for traffic_allocation in traffic_allocations: + current_end_of_range = traffic_allocation.get('endOfRange') + if bucketing_number < current_end_of_range: + return traffic_allocation.get('entityId') - return None + return None - def bucket(self, project_config, experiment, user_id, bucketing_id): - """ For a given experiment and bucketing ID determines variation to be shown to user. + def bucket(self, project_config, experiment, user_id, bucketing_id): + """ For a given experiment and bucketing ID determines variation to be shown to user. Args: project_config: Instance of ProjectConfig. @@ -100,45 +100,41 @@ def bucket(self, project_config, experiment, user_id, bucketing_id): Variation in which user with ID user_id will be put in. None if no variation. """ - if not experiment: - return None - - # Determine if experiment is in a mutually exclusive group - if experiment.groupPolicy in GROUP_POLICIES: - group = project_config.get_group(experiment.groupId) - - if not group: + if not experiment: + return None + + # Determine if experiment is in a mutually exclusive group + if experiment.groupPolicy in GROUP_POLICIES: + group = project_config.get_group(experiment.groupId) + + if not group: + return None + + user_experiment_id = self.find_bucket( + project_config, bucketing_id, experiment.groupId, group.trafficAllocation, + ) + if not user_experiment_id: + project_config.logger.info('User "%s" is in no experiment.' % user_id) + return None + + if user_experiment_id != experiment.id: + project_config.logger.info( + 'User "%s" is not in experiment "%s" of group %s.' % (user_id, experiment.key, experiment.groupId) + ) + return None + + project_config.logger.info( + 'User "%s" is in experiment %s of group %s.' % (user_id, experiment.key, experiment.groupId) + ) + + # Bucket user if not in white-list and in group (if any) + variation_id = self.find_bucket(project_config, bucketing_id, experiment.id, experiment.trafficAllocation) + if variation_id: + variation = project_config.get_variation_from_id(experiment.key, variation_id) + project_config.logger.info( + 'User "%s" is in variation "%s" of experiment %s.' % (user_id, variation.key, experiment.key) + ) + return variation + + project_config.logger.info('User "%s" is in no variation.' % user_id) return None - - user_experiment_id = self.find_bucket(project_config, bucketing_id, experiment.groupId, group.trafficAllocation) - if not user_experiment_id: - project_config.logger.info('User "%s" is in no experiment.' % user_id) - return None - - if user_experiment_id != experiment.id: - project_config.logger.info('User "%s" is not in experiment "%s" of group %s.' % ( - user_id, - experiment.key, - experiment.groupId - )) - return None - - project_config.logger.info('User "%s" is in experiment %s of group %s.' % ( - user_id, - experiment.key, - experiment.groupId - )) - - # Bucket user if not in white-list and in group (if any) - variation_id = self.find_bucket(project_config, bucketing_id, experiment.id, experiment.trafficAllocation) - if variation_id: - variation = project_config.get_variation_from_id(experiment.key, variation_id) - project_config.logger.info('User "%s" is in variation "%s" of experiment %s.' % ( - user_id, - variation.key, - experiment.key - )) - return variation - - project_config.logger.info('User "%s" is in no variation.' % user_id) - return None diff --git a/optimizely/config_manager.py b/optimizely/config_manager.py index 9724fa2f..b1e5b02d 100644 --- a/optimizely/config_manager.py +++ b/optimizely/config_manager.py @@ -33,10 +33,7 @@ class BaseConfigManager(ABC): """ Base class for Optimizely's config manager. """ - def __init__(self, - logger=None, - error_handler=None, - notification_center=None): + def __init__(self, logger=None, error_handler=None, notification_center=None): """ Initialize config manager. Args: @@ -74,12 +71,9 @@ def get_config(self): class StaticConfigManager(BaseConfigManager): """ Config manager that returns ProjectConfig based on provided datafile. """ - def __init__(self, - datafile=None, - logger=None, - error_handler=None, - notification_center=None, - skip_json_validation=False): + def __init__( + self, datafile=None, logger=None, error_handler=None, notification_center=None, skip_json_validation=False, + ): """ Initialize config manager. Datafile has to be provided to use. Args: @@ -91,9 +85,9 @@ def __init__(self, validation upon object invocation. By default JSON schema validation will be performed. """ - super(StaticConfigManager, self).__init__(logger=logger, - error_handler=error_handler, - notification_center=notification_center) + super(StaticConfigManager, self).__init__( + logger=logger, error_handler=error_handler, notification_center=notification_center, + ) self._config = None self.validate_schema = not skip_json_validation self._set_config(datafile) @@ -153,17 +147,19 @@ def get_config(self): class PollingConfigManager(StaticConfigManager): """ Config manager that polls for the datafile and updated ProjectConfig based on an update interval. """ - def __init__(self, - sdk_key=None, - datafile=None, - update_interval=None, - blocking_timeout=None, - url=None, - url_template=None, - logger=None, - error_handler=None, - notification_center=None, - skip_json_validation=False): + def __init__( + self, + sdk_key=None, + datafile=None, + update_interval=None, + blocking_timeout=None, + url=None, + url_template=None, + logger=None, + error_handler=None, + notification_center=None, + skip_json_validation=False, + ): """ Initialize config manager. One of sdk_key or url has to be set to be able to use. Args: @@ -185,13 +181,16 @@ def __init__(self, """ self._config_ready_event = threading.Event() - super(PollingConfigManager, self).__init__(datafile=datafile, - logger=logger, - error_handler=error_handler, - notification_center=notification_center, - skip_json_validation=skip_json_validation) - self.datafile_url = self.get_datafile_url(sdk_key, url, - url_template or enums.ConfigManager.DATAFILE_URL_TEMPLATE) + super(PollingConfigManager, self).__init__( + datafile=datafile, + logger=logger, + error_handler=error_handler, + notification_center=notification_center, + skip_json_validation=skip_json_validation, + ) + self.datafile_url = self.get_datafile_url( + sdk_key, url, url_template or enums.ConfigManager.DATAFILE_URL_TEMPLATE + ) self.set_update_interval(update_interval) self.set_blocking_timeout(blocking_timeout) self.last_modified = None @@ -227,7 +226,8 @@ def get_datafile_url(sdk_key, url, url_template): return url_template.format(sdk_key=sdk_key) except (AttributeError, KeyError): raise optimizely_exceptions.InvalidInputException( - 'Invalid url_template {} provided.'.format(url_template)) + 'Invalid url_template {} provided.'.format(url_template) + ) return url @@ -238,8 +238,8 @@ def _set_config(self, datafile): datafile: JSON string representing the Optimizely project. """ if datafile or self._config_ready_event.is_set(): - super(PollingConfigManager, self)._set_config(datafile=datafile) - self._config_ready_event.set() + super(PollingConfigManager, self)._set_config(datafile=datafile) + self._config_ready_event.set() def get_config(self): """ Returns instance of ProjectConfig. Returns immediately if project config is ready otherwise @@ -269,9 +269,10 @@ def set_update_interval(self, update_interval): # If polling interval is less than or equal to 0 then set it to default update interval. if update_interval <= 0: - self.logger.debug('update_interval value {} too small. Defaulting to {}'.format( - update_interval, - enums.ConfigManager.DEFAULT_UPDATE_INTERVAL) + self.logger.debug( + 'update_interval value {} too small. Defaulting to {}'.format( + update_interval, enums.ConfigManager.DEFAULT_UPDATE_INTERVAL + ) ) update_interval = enums.ConfigManager.DEFAULT_UPDATE_INTERVAL @@ -294,9 +295,10 @@ def set_blocking_timeout(self, blocking_timeout): # If blocking timeout is less than 0 then set it to default blocking timeout. if blocking_timeout < 0: - self.logger.debug('blocking timeout value {} too small. Defaulting to {}'.format( - blocking_timeout, - enums.ConfigManager.DEFAULT_BLOCKING_TIMEOUT) + self.logger.debug( + 'blocking timeout value {} too small. Defaulting to {}'.format( + blocking_timeout, enums.ConfigManager.DEFAULT_BLOCKING_TIMEOUT + ) ) blocking_timeout = enums.ConfigManager.DEFAULT_BLOCKING_TIMEOUT @@ -337,9 +339,9 @@ def fetch_datafile(self): if self.last_modified: request_headers[enums.HTTPHeaders.IF_MODIFIED_SINCE] = self.last_modified - response = requests.get(self.datafile_url, - headers=request_headers, - timeout=enums.ConfigManager.REQUEST_TIMEOUT) + response = requests.get( + self.datafile_url, headers=request_headers, timeout=enums.ConfigManager.REQUEST_TIMEOUT, + ) self._handle_response(response) @property @@ -350,12 +352,13 @@ def is_running(self): def _run(self): """ Triggered as part of the thread which fetches the datafile and sleeps until next update interval. """ try: - while self.is_running: - self.fetch_datafile() - time.sleep(self.update_interval) + while self.is_running: + self.fetch_datafile() + time.sleep(self.update_interval) except (OSError, OverflowError) as err: - self.logger.error('Error in time.sleep. ' - 'Provided update_interval value may be too big. Error: {}'.format(str(err))) + self.logger.error( + 'Error in time.sleep. ' 'Provided update_interval value may be too big. Error: {}'.format(str(err)) + ) raise def start(self): diff --git a/optimizely/decision_service.py b/optimizely/decision_service.py index d8b08f9e..2e813747 100644 --- a/optimizely/decision_service.py +++ b/optimizely/decision_service.py @@ -25,21 +25,21 @@ class DecisionService(object): - """ Class encapsulating all decision related capabilities. """ + """ Class encapsulating all decision related capabilities. """ - def __init__(self, logger, user_profile_service): - self.bucketer = bucketer.Bucketer() - self.logger = logger - self.user_profile_service = user_profile_service + def __init__(self, logger, user_profile_service): + self.bucketer = bucketer.Bucketer() + self.logger = logger + self.user_profile_service = user_profile_service - # Map of user IDs to another map of experiments to variations. - # This contains all the forced variations set by the user - # by calling set_forced_variation (it is not the same as the - # whitelisting forcedVariations data structure). - self.forced_variation_map = {} + # Map of user IDs to another map of experiments to variations. + # This contains all the forced variations set by the user + # by calling set_forced_variation (it is not the same as the + # whitelisting forcedVariations data structure). + self.forced_variation_map = {} - def _get_bucketing_id(self, user_id, attributes): - """ Helper method to determine bucketing ID for the user. + def _get_bucketing_id(self, user_id, attributes): + """ Helper method to determine bucketing ID for the user. Args: user_id: ID for user. @@ -49,19 +49,19 @@ def _get_bucketing_id(self, user_id, attributes): String representing bucketing ID if it is a String type in attributes else return user ID. """ - attributes = attributes or {} - bucketing_id = attributes.get(enums.ControlAttributes.BUCKETING_ID) + attributes = attributes or {} + bucketing_id = attributes.get(enums.ControlAttributes.BUCKETING_ID) - if bucketing_id is not None: - if isinstance(bucketing_id, string_types): - return bucketing_id + if bucketing_id is not None: + if isinstance(bucketing_id, string_types): + return bucketing_id - self.logger.warning('Bucketing ID attribute is not a string. Defaulted to user_id.') + self.logger.warning('Bucketing ID attribute is not a string. Defaulted to user_id.') - return user_id + return user_id - def set_forced_variation(self, project_config, experiment_key, user_id, variation_key): - """ Sets users to a map of experiments to forced variations. + 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. @@ -72,55 +72,54 @@ def set_forced_variation(self, project_config, experiment_key, user_id, variatio 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. - return False - - experiment_id = experiment.id - if variation_key is None: - if user_id in self.forced_variation_map: - experiment_to_variation_map = self.forced_variation_map.get(user_id) - if experiment_id in experiment_to_variation_map: - del(self.forced_variation_map[user_id][experiment_id]) - self.logger.debug('Variation mapped to experiment "%s" has been removed for user "%s".' % ( - experiment_key, - user_id - )) + experiment = project_config.get_experiment_from_key(experiment_key) + if not experiment: + # The invalid experiment key will be logged inside this call. + return False + + experiment_id = experiment.id + if variation_key is None: + if user_id in self.forced_variation_map: + experiment_to_variation_map = self.forced_variation_map.get(user_id) + if experiment_id in experiment_to_variation_map: + del self.forced_variation_map[user_id][experiment_id] + self.logger.debug( + 'Variation mapped to experiment "%s" has been removed for user "%s".' + % (experiment_key, user_id) + ) + else: + self.logger.debug( + 'Nothing to remove. Variation mapped to experiment "%s" for user "%s" does not exist.' + % (experiment_key, user_id) + ) + else: + self.logger.debug('Nothing to remove. User "%s" does not exist in the forced variation map.' % user_id) + return True + + if not validator.is_non_empty_string(variation_key): + self.logger.debug('Variation key is invalid.') + return False + + forced_variation = project_config.get_variation_from_key(experiment_key, variation_key) + if not forced_variation: + # The invalid variation key will be logged inside this call. + return False + + variation_id = forced_variation.id + + if user_id not in self.forced_variation_map: + self.forced_variation_map[user_id] = {experiment_id: variation_id} else: - self.logger.debug('Nothing to remove. Variation mapped to experiment "%s" for user "%s" does not exist.' % ( - experiment_key, - user_id - )) - else: - self.logger.debug('Nothing to remove. User "%s" does not exist in the forced variation map.' % user_id) - return True - - if not validator.is_non_empty_string(variation_key): - self.logger.debug('Variation key is invalid.') - return False - - forced_variation = project_config.get_variation_from_key(experiment_key, variation_key) - if not forced_variation: - # The invalid variation key will be logged inside this call. - return False - - variation_id = forced_variation.id - - if user_id not in self.forced_variation_map: - self.forced_variation_map[user_id] = {experiment_id: variation_id} - else: - self.forced_variation_map[user_id][experiment_id] = variation_id - - self.logger.debug('Set variation "%s" for experiment "%s" and user "%s" in the forced variation map.' % ( - variation_id, - experiment_id, - user_id - )) - return True - - def get_forced_variation(self, project_config, experiment_key, user_id): - """ Gets the forced variation key for the given user and experiment. + self.forced_variation_map[user_id][experiment_id] = variation_id + + self.logger.debug( + 'Set variation "%s" for experiment "%s" and user "%s" in the forced variation map.' + % (variation_id, experiment_id, user_id) + ) + return True + + 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. @@ -131,42 +130,38 @@ def get_forced_variation(self, project_config, experiment_key, user_id): The variation which the given user and experiment should be forced into. """ - if user_id not in self.forced_variation_map: - self.logger.debug('User "%s" is not in the forced variation map.' % user_id) - return None - - experiment = project_config.get_experiment_from_key(experiment_key) - if not experiment: - # The invalid experiment key will be logged inside this call. - return None - - experiment_to_variation_map = self.forced_variation_map.get(user_id) - - if not experiment_to_variation_map: - self.logger.debug('No experiment "%s" mapped to user "%s" in the forced variation map.' % ( - experiment_key, - user_id - )) - return None - - variation_id = experiment_to_variation_map.get(experiment.id) - if variation_id is None: - self.logger.debug( - 'No variation mapped to experiment "%s" in the forced variation map.' % experiment_key - ) - return None - - variation = project_config.get_variation_from_id(experiment_key, variation_id) - - self.logger.debug('Variation "%s" is mapped to experiment "%s" and user "%s" in the forced variation map' % ( - variation.key, - experiment_key, - user_id - )) - return variation - - def get_whitelisted_variation(self, project_config, experiment, user_id): - """ Determine if a user is forced into a variation (through whitelisting) + if user_id not in self.forced_variation_map: + self.logger.debug('User "%s" is not in the forced variation map.' % user_id) + return None + + experiment = project_config.get_experiment_from_key(experiment_key) + if not experiment: + # The invalid experiment key will be logged inside this call. + return None + + experiment_to_variation_map = self.forced_variation_map.get(user_id) + + if not experiment_to_variation_map: + self.logger.debug( + 'No experiment "%s" mapped to user "%s" in the forced variation map.' % (experiment_key, user_id) + ) + return None + + variation_id = experiment_to_variation_map.get(experiment.id) + if variation_id is None: + self.logger.debug('No variation mapped to experiment "%s" in the forced variation map.' % experiment_key) + return None + + variation = project_config.get_variation_from_id(experiment_key, variation_id) + + self.logger.debug( + 'Variation "%s" is mapped to experiment "%s" and user "%s" in the forced variation map' + % (variation.key, experiment_key, user_id) + ) + return variation + + 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: @@ -178,18 +173,18 @@ def get_whitelisted_variation(self, project_config, experiment, user_id): Variation in which the user with ID user_id is forced into. None if no variation. """ - 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: - self.logger.info('User "%s" is forced in variation "%s".' % (user_id, variation_key)) - return variation + 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: + self.logger.info('User "%s" is forced in variation "%s".' % (user_id, variation_key)) + return variation - return None + return None - 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. + 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. @@ -200,23 +195,22 @@ def get_stored_variation(self, project_config, experiment, user_profile): 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: - self.logger.info('Found a stored decision. User "%s" is in variation "%s" of experiment "%s".' % ( - user_id, - variation.key, - experiment.key - )) - return variation + user_id = user_profile.user_id + variation_id = user_profile.get_variation_for_experiment(experiment.id) - return None + if variation_id: + variation = project_config.get_variation_from_id(experiment.key, variation_id) + if variation: + self.logger.info( + 'Found a stored decision. User "%s" is in variation "%s" of experiment "%s".' + % (user_id, variation.key, experiment.key) + ) + return variation - def get_variation(self, project_config, experiment, user_id, attributes, ignore_user_profile=False): - """ Top-level function to help determine variation user should be put in. + return None + + def get_variation(self, project_config, experiment, user_id, attributes, ignore_user_profile=False): + """ 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. @@ -235,64 +229,61 @@ def get_variation(self, project_config, experiment, user_id, attributes, ignore_ Variation user should see. None if user is not in experiment or experiment is not running. """ - # Check if experiment is running - if not experiment_helper.is_experiment_running(experiment): - self.logger.info('Experiment "%s" is not running.' % experiment.key) - return None - - # Check if the user is forced into a variation - variation = self.get_forced_variation(project_config, experiment.key, user_id) - if variation: - return variation - - # Check to see if user is white-listed for a certain variation - variation = self.get_whitelisted_variation(project_config, experiment, user_id) - if variation: - return variation - - # Check to see if user has a decision available for the given experiment - user_profile = UserProfile(user_id) - if not ignore_user_profile and self.user_profile_service: - try: - retrieved_profile = self.user_profile_service.lookup(user_id) - except: - self.logger.exception('Unable to retrieve user profile for user "%s" as lookup failed.' % user_id) - retrieved_profile = None - - if validator.is_user_profile_valid(retrieved_profile): - user_profile = UserProfile(**retrieved_profile) - variation = self.get_stored_variation(project_config, experiment, user_profile) + # Check if experiment is running + if not experiment_helper.is_experiment_running(experiment): + self.logger.info('Experiment "%s" is not running.' % experiment.key) + return None + + # Check if the user is forced into a variation + variation = self.get_forced_variation(project_config, experiment.key, user_id) + if variation: + return variation + + # Check to see if user is white-listed for a certain variation + variation = self.get_whitelisted_variation(project_config, experiment, user_id) + if variation: + return variation + + # Check to see if user has a decision available for the given experiment + user_profile = UserProfile(user_id) + if not ignore_user_profile and self.user_profile_service: + try: + retrieved_profile = self.user_profile_service.lookup(user_id) + except: + self.logger.exception('Unable to retrieve user profile for user "%s" as lookup failed.' % user_id) + retrieved_profile = None + + if validator.is_user_profile_valid(retrieved_profile): + user_profile = UserProfile(**retrieved_profile) + variation = self.get_stored_variation(project_config, experiment, user_profile) + if variation: + return variation + else: + self.logger.warning('User profile has invalid format.') + + # Bucket user and store the new decision + if not audience_helper.is_user_in_experiment(project_config, experiment, attributes, self.logger): + self.logger.info('User "%s" does not meet conditions to be in experiment "%s".' % (user_id, experiment.key)) + return None + + # Determine bucketing ID to be used + bucketing_id = self._get_bucketing_id(user_id, attributes) + variation = self.bucketer.bucket(project_config, experiment, user_id, bucketing_id) + if variation: - return variation - else: - self.logger.warning('User profile has invalid format.') - - # Bucket user and store the new decision - if not audience_helper.is_user_in_experiment(project_config, experiment, attributes, self.logger): - self.logger.info('User "%s" does not meet conditions to be in experiment "%s".' % ( - user_id, - experiment.key - )) - return None - - # Determine bucketing ID to be used - bucketing_id = self._get_bucketing_id(user_id, attributes) - variation = self.bucketer.bucket(project_config, experiment, user_id, bucketing_id) - - if variation: - # Store this new decision and return the variation for the user - if not ignore_user_profile and self.user_profile_service: - try: - user_profile.save_variation_for_experiment(experiment.id, variation.id) - self.user_profile_service.save(user_profile.__dict__) - except: - self.logger.exception('Unable to save user profile for user "%s".' % user_id) - return variation - - return None - - def get_variation_for_rollout(self, project_config, rollout, user_id, attributes=None): - """ Determine which experiment/variation the user is in for a given rollout. + # Store this new decision and return the variation for the user + if not ignore_user_profile and self.user_profile_service: + try: + user_profile.save_variation_for_experiment(experiment.id, variation.id) + self.user_profile_service.save(user_profile.__dict__) + except: + self.logger.exception('Unable to save user profile for user "%s".' % user_id) + return variation + + return None + + def get_variation_for_rollout(self, project_config, rollout, user_id, attributes=None): + """ Determine which experiment/variation the user is in for a given rollout. Returns the variation of the first experiment the user qualifies for. Args: @@ -305,54 +296,52 @@ def get_variation_for_rollout(self, project_config, rollout, user_id, attributes Decision namedtuple consisting of experiment and variation for the user. """ - # 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): - experiment = project_config.get_experiment_from_key(rollout.experiments[idx].get('key')) - - # Check if user meets audience conditions for targeting rule - if not audience_helper.is_user_in_experiment(project_config, experiment, attributes, self.logger): - self.logger.debug('User "%s" does not meet conditions for targeting rule %s.' % ( - user_id, - idx + 1 - )) - continue - - self.logger.debug('User "%s" meets conditions for targeting rule %s.' % (user_id, idx + 1)) - # Determine bucketing ID to be used - bucketing_id = self._get_bucketing_id(user_id, attributes) - variation = self.bucketer.bucket(project_config, experiment, user_id, bucketing_id) - if variation: - self.logger.debug('User "%s" is in variation %s of experiment %s.' % ( - user_id, - variation.key, - experiment.key - )) - return Decision(experiment, variation, enums.DecisionSources.ROLLOUT) - else: - # Evaluate no further rules - self.logger.debug('User "%s" is not in the traffic group for the targeting else. ' - 'Checking "Everyone Else" rule now.' % user_id) - break - - # Evaluate last rule i.e. "Everyone Else" rule - everyone_else_experiment = project_config.get_experiment_from_key(rollout.experiments[-1].get('key')) - if audience_helper.is_user_in_experiment( - project_config, - project_config.get_experiment_from_key(rollout.experiments[-1].get('key')), - attributes, - self.logger): - # Determine bucketing ID to be used - bucketing_id = self._get_bucketing_id(user_id, attributes) - variation = self.bucketer.bucket(project_config, everyone_else_experiment, user_id, bucketing_id) - if variation: - self.logger.debug('User "%s" meets conditions for targeting rule "Everyone Else".' % user_id) - return Decision(everyone_else_experiment, variation, enums.DecisionSources.ROLLOUT) - - return Decision(None, None, enums.DecisionSources.ROLLOUT) - - def get_experiment_in_group(self, project_config, group, bucketing_id): - """ Determine which experiment in the group the user is bucketed into. + # 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): + experiment = project_config.get_experiment_from_key(rollout.experiments[idx].get('key')) + + # Check if user meets audience conditions for targeting rule + if not audience_helper.is_user_in_experiment(project_config, experiment, attributes, self.logger): + self.logger.debug('User "%s" does not meet conditions for targeting rule %s.' % (user_id, idx + 1)) + continue + + self.logger.debug('User "%s" meets conditions for targeting rule %s.' % (user_id, idx + 1)) + # Determine bucketing ID to be used + bucketing_id = self._get_bucketing_id(user_id, attributes) + variation = self.bucketer.bucket(project_config, experiment, user_id, bucketing_id) + if variation: + self.logger.debug( + 'User "%s" is in variation %s of experiment %s.' % (user_id, variation.key, experiment.key) + ) + return Decision(experiment, variation, enums.DecisionSources.ROLLOUT) + else: + # Evaluate no further rules + self.logger.debug( + 'User "%s" is not in the traffic group for the targeting else. ' + 'Checking "Everyone Else" rule now.' % user_id + ) + break + + # Evaluate last rule i.e. "Everyone Else" rule + everyone_else_experiment = project_config.get_experiment_from_key(rollout.experiments[-1].get('key')) + if audience_helper.is_user_in_experiment( + project_config, + project_config.get_experiment_from_key(rollout.experiments[-1].get('key')), + attributes, + self.logger, + ): + # Determine bucketing ID to be used + bucketing_id = self._get_bucketing_id(user_id, attributes) + variation = self.bucketer.bucket(project_config, everyone_else_experiment, user_id, bucketing_id) + if variation: + self.logger.debug('User "%s" meets conditions for targeting rule "Everyone Else".' % user_id) + return Decision(everyone_else_experiment, variation, enums.DecisionSources.ROLLOUT,) + + return Decision(None, None, enums.DecisionSources.ROLLOUT) + + def get_experiment_in_group(self, project_config, group, bucketing_id): + """ Determine which experiment in the group the user is bucketed into. Args: project_config: Instance of ProjectConfig. @@ -363,26 +352,24 @@ def get_experiment_in_group(self, project_config, group, bucketing_id): Experiment if the user is bucketed into an experiment in the specified group. None otherwise. """ - experiment_id = self.bucketer.find_bucket(project_config, bucketing_id, group.id, group.trafficAllocation) - if experiment_id: - experiment = project_config.get_experiment_from_id(experiment_id) - if experiment: - self.logger.info('User with bucketing ID "%s" is in experiment %s of group %s.' % ( - bucketing_id, - experiment.key, - group.id - )) - return experiment + experiment_id = self.bucketer.find_bucket(project_config, bucketing_id, group.id, group.trafficAllocation) + if experiment_id: + experiment = project_config.get_experiment_from_id(experiment_id) + if experiment: + self.logger.info( + 'User with bucketing ID "%s" is in experiment %s of group %s.' + % (bucketing_id, experiment.key, group.id) + ) + return experiment - self.logger.info('User with bucketing ID "%s" is not in any experiments of group %s.' % ( - bucketing_id, - group.id - )) + self.logger.info( + 'User with bucketing ID "%s" is not in any experiments of group %s.' % (bucketing_id, group.id) + ) - return None + return None - def get_variation_for_feature(self, project_config, feature, user_id, attributes=None): - """ Returns the experiment/variation the user is bucketed in for the given feature. + def get_variation_for_feature(self, project_config, feature, user_id, attributes=None): + """ Returns the experiment/variation the user is bucketed in for the given feature. Args: project_config: Instance of ProjectConfig. @@ -394,44 +381,40 @@ def get_variation_for_feature(self, project_config, feature, user_id, attributes Decision namedtuple consisting of experiment and variation for the user. """ - bucketing_id = self._get_bucketing_id(user_id, attributes) - - # First check if the feature is in a mutex group - if feature.groupId: - group = project_config.get_group(feature.groupId) - if group: - experiment = self.get_experiment_in_group(project_config, group, bucketing_id) - if experiment and experiment.id in feature.experimentIds: - variation = self.get_variation(project_config, experiment, user_id, attributes) - - if variation: - self.logger.debug('User "%s" is in variation %s of experiment %s.' % ( - user_id, - variation.key, - experiment.key - )) - return Decision(experiment, variation, enums.DecisionSources.FEATURE_TEST) - else: - self.logger.error(enums.Errors.INVALID_GROUP_ID.format('_get_variation_for_feature')) - - # Next check if the feature is being experimented on - elif feature.experimentIds: - # If an experiment is not in a group, then the feature can only be associated with one experiment - experiment = project_config.get_experiment_from_id(feature.experimentIds[0]) - if experiment: - variation = self.get_variation(project_config, experiment, user_id, attributes) + bucketing_id = self._get_bucketing_id(user_id, attributes) - if variation: - self.logger.debug('User "%s" is in variation %s of experiment %s.' % ( - user_id, - variation.key, - experiment.key - )) - return Decision(experiment, variation, enums.DecisionSources.FEATURE_TEST) - - # 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) + # First check if the feature is in a mutex group + if feature.groupId: + group = project_config.get_group(feature.groupId) + if group: + experiment = self.get_experiment_in_group(project_config, group, bucketing_id) + if experiment and experiment.id in feature.experimentIds: + variation = self.get_variation(project_config, experiment, user_id, attributes) + + if variation: + self.logger.debug( + 'User "%s" is in variation %s of experiment %s.' % (user_id, variation.key, experiment.key) + ) + return Decision(experiment, variation, enums.DecisionSources.FEATURE_TEST) + else: + self.logger.error(enums.Errors.INVALID_GROUP_ID.format('_get_variation_for_feature')) + + # Next check if the feature is being experimented on + elif feature.experimentIds: + # If an experiment is not in a group, then the feature can only be associated with one experiment + experiment = project_config.get_experiment_from_id(feature.experimentIds[0]) + if experiment: + variation = self.get_variation(project_config, experiment, user_id, attributes) + + if variation: + self.logger.debug( + 'User "%s" is in variation %s of experiment %s.' % (user_id, variation.key, experiment.key) + ) + return Decision(experiment, variation, enums.DecisionSources.FEATURE_TEST) + + # 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) diff --git a/optimizely/entities.py b/optimizely/entities.py index 541838a5..75c73845 100644 --- a/optimizely/entities.py +++ b/optimizely/entities.py @@ -13,109 +13,111 @@ class BaseEntity(object): - - def __eq__(self, other): - return self.__dict__ == other.__dict__ + def __eq__(self, other): + return self.__dict__ == other.__dict__ class Attribute(BaseEntity): - - def __init__(self, id, key, **kwargs): - self.id = id - self.key = key + def __init__(self, id, key, **kwargs): + self.id = id + self.key = key class Audience(BaseEntity): - - def __init__(self, id, name, conditions, conditionStructure=None, conditionList=None, **kwargs): - self.id = id - self.name = name - self.conditions = conditions - self.conditionStructure = conditionStructure - self.conditionList = conditionList + def __init__(self, id, name, conditions, conditionStructure=None, conditionList=None, **kwargs): + self.id = id + self.name = name + self.conditions = conditions + self.conditionStructure = conditionStructure + self.conditionList = conditionList class Event(BaseEntity): - - def __init__(self, id, key, experimentIds, **kwargs): - self.id = id - self.key = key - self.experimentIds = experimentIds + def __init__(self, id, key, experimentIds, **kwargs): + self.id = id + self.key = key + self.experimentIds = experimentIds class Experiment(BaseEntity): - - def __init__(self, id, key, status, audienceIds, variations, forcedVariations, - trafficAllocation, layerId, audienceConditions=None, groupId=None, groupPolicy=None, **kwargs): - self.id = id - self.key = key - self.status = status - self.audienceIds = audienceIds - self.audienceConditions = audienceConditions - self.variations = variations - self.forcedVariations = forcedVariations - self.trafficAllocation = trafficAllocation - self.layerId = layerId - self.groupId = groupId - self.groupPolicy = groupPolicy - - def getAudienceConditionsOrIds(self): - """ Returns audienceConditions if present, otherwise audienceIds. """ - return self.audienceConditions if self.audienceConditions is not None else self.audienceIds + def __init__( + self, + id, + key, + status, + audienceIds, + variations, + forcedVariations, + trafficAllocation, + layerId, + audienceConditions=None, + groupId=None, + groupPolicy=None, + **kwargs + ): + self.id = id + self.key = key + self.status = status + self.audienceIds = audienceIds + self.audienceConditions = audienceConditions + self.variations = variations + self.forcedVariations = forcedVariations + self.trafficAllocation = trafficAllocation + self.layerId = layerId + self.groupId = groupId + self.groupPolicy = groupPolicy + + def getAudienceConditionsOrIds(self): + """ Returns audienceConditions if present, otherwise audienceIds. """ + return self.audienceConditions if self.audienceConditions is not None else self.audienceIds class FeatureFlag(BaseEntity): - - def __init__(self, id, key, experimentIds, rolloutId, variables, groupId=None, **kwargs): - self.id = id - self.key = key - self.experimentIds = experimentIds - self.rolloutId = rolloutId - self.variables = variables - self.groupId = groupId + def __init__(self, id, key, experimentIds, rolloutId, variables, groupId=None, **kwargs): + self.id = id + self.key = key + self.experimentIds = experimentIds + self.rolloutId = rolloutId + self.variables = variables + self.groupId = groupId class Group(BaseEntity): - - def __init__(self, id, policy, experiments, trafficAllocation, **kwargs): - self.id = id - self.policy = policy - self.experiments = experiments - self.trafficAllocation = trafficAllocation + def __init__(self, id, policy, experiments, trafficAllocation, **kwargs): + self.id = id + self.policy = policy + self.experiments = experiments + self.trafficAllocation = trafficAllocation class Layer(BaseEntity): - - def __init__(self, id, experiments, **kwargs): - self.id = id - self.experiments = experiments + def __init__(self, id, experiments, **kwargs): + self.id = id + self.experiments = experiments class Variable(BaseEntity): + class Type(object): + BOOLEAN = 'boolean' + DOUBLE = 'double' + INTEGER = 'integer' + STRING = 'string' - class Type(object): - BOOLEAN = 'boolean' - DOUBLE = 'double' - INTEGER = 'integer' - STRING = 'string' - - def __init__(self, id, key, type, defaultValue, **kwargs): - self.id = id - self.key = key - self.type = type - self.defaultValue = defaultValue + def __init__(self, id, key, type, defaultValue, **kwargs): + self.id = id + self.key = key + self.type = type + self.defaultValue = defaultValue class Variation(BaseEntity): - - class VariableUsage(BaseEntity): - - def __init__(self, id, value, **kwards): - self.id = id - self.value = value - - def __init__(self, id, key, featureEnabled=False, variables=None, **kwargs): - self.id = id - self.key = key - self.featureEnabled = featureEnabled - self.variables = variables or [] + class VariableUsage(BaseEntity): + def __init__(self, id, value, **kwards): + self.id = id + self.value = value + + def __init__(self, id, key, featureEnabled=False, variables=None, **kwargs): + self.id = id + self.key = key + self.featureEnabled = featureEnabled + self.variables = variables or [] diff --git a/optimizely/error_handler.py b/optimizely/error_handler.py index 452ac1d8..ed88625e 100644 --- a/optimizely/error_handler.py +++ b/optimizely/error_handler.py @@ -13,21 +13,21 @@ class BaseErrorHandler(object): - """ Class encapsulating exception handling functionality. + """ Class encapsulating exception handling functionality. Override with your own exception handler providing handle_error method. """ - @staticmethod - def handle_error(*args): - pass + @staticmethod + def handle_error(*args): + pass class NoOpErrorHandler(BaseErrorHandler): - """ Class providing handle_error method which suppresses the error. """ + """ Class providing handle_error method which suppresses the error. """ class RaiseExceptionErrorHandler(BaseErrorHandler): - """ Class providing handle_error method which raises provided exception. """ + """ Class providing handle_error method which raises provided exception. """ - @staticmethod - def handle_error(error): - raise error + @staticmethod + def handle_error(error): + raise error diff --git a/optimizely/event/event_factory.py b/optimizely/event/event_factory.py index 2489dc92..e2851bfc 100644 --- a/optimizely/event/event_factory.py +++ b/optimizely/event/event_factory.py @@ -22,19 +22,19 @@ class EventFactory(object): - """ EventFactory builds LogEvent object from a given UserEvent. + """ EventFactory builds LogEvent object from a given UserEvent. This class serves to separate concerns between events in the SDK and the API used to record the events via the Optimizely Events API ("https://developers.optimizely.com/x/events/api/index.html") """ - EVENT_ENDPOINT = 'https://logx.optimizely.com/v1/events' - HTTP_VERB = 'POST' - HTTP_HEADERS = {'Content-Type': 'application/json'} - ACTIVATE_EVENT_KEY = 'campaign_activated' + EVENT_ENDPOINT = 'https://logx.optimizely.com/v1/events' + HTTP_VERB = 'POST' + HTTP_HEADERS = {'Content-Type': 'application/json'} + ACTIVATE_EVENT_KEY = 'campaign_activated' - @classmethod - def create_log_event(cls, user_events, logger): - """ Create LogEvent instance. + @classmethod + def create_log_event(cls, user_events, logger): + """ Create LogEvent instance. Args: user_events: A single UserEvent instance or a list of UserEvent instances. @@ -44,40 +44,40 @@ def create_log_event(cls, user_events, logger): LogEvent instance. """ - if not isinstance(user_events, list): - user_events = [user_events] + if not isinstance(user_events, list): + user_events = [user_events] - visitors = [] + visitors = [] - for event in user_events: - visitor = cls._create_visitor(event, logger) + for event in user_events: + visitor = cls._create_visitor(event, logger) - if visitor: - visitors.append(visitor) + if visitor: + visitors.append(visitor) - if len(visitors) == 0: - return None + if len(visitors) == 0: + return None - user_context = user_events[0].event_context - event_batch = payload.EventBatch( - user_context.account_id, - user_context.project_id, - user_context.revision, - user_context.client_name, - user_context.client_version, - user_context.anonymize_ip, - True - ) + user_context = user_events[0].event_context + event_batch = payload.EventBatch( + user_context.account_id, + user_context.project_id, + user_context.revision, + user_context.client_name, + user_context.client_version, + user_context.anonymize_ip, + True, + ) - event_batch.visitors = visitors + event_batch.visitors = visitors - event_params = event_batch.get_event_params() + event_params = event_batch.get_event_params() - return log_event.LogEvent(cls.EVENT_ENDPOINT, event_params, cls.HTTP_VERB, cls.HTTP_HEADERS) + return log_event.LogEvent(cls.EVENT_ENDPOINT, event_params, cls.HTTP_VERB, cls.HTTP_HEADERS) - @classmethod - def _create_visitor(cls, event, logger): - """ Helper method to create Visitor instance for event_batch. + @classmethod + def _create_visitor(cls, event, logger): + """ Helper method to create Visitor instance for event_batch. Args: event: Instance of UserEvent. @@ -88,53 +88,40 @@ def _create_visitor(cls, event, logger): - event is invalid. """ - if isinstance(event, user_event.ImpressionEvent): - decision = payload.Decision( - event.experiment.layerId, - event.experiment.id, - event.variation.id, - ) + if isinstance(event, user_event.ImpressionEvent): + decision = payload.Decision(event.experiment.layerId, event.experiment.id, event.variation.id,) - snapshot_event = payload.SnapshotEvent( - event.experiment.layerId, - event.uuid, - cls.ACTIVATE_EVENT_KEY, - event.timestamp - ) + snapshot_event = payload.SnapshotEvent( + event.experiment.layerId, event.uuid, cls.ACTIVATE_EVENT_KEY, event.timestamp, + ) - snapshot = payload.Snapshot([snapshot_event], [decision]) + snapshot = payload.Snapshot([snapshot_event], [decision]) - visitor = payload.Visitor([snapshot], event.visitor_attributes, event.user_id) + visitor = payload.Visitor([snapshot], event.visitor_attributes, event.user_id) - return visitor + return visitor - elif isinstance(event, user_event.ConversionEvent): - revenue = event_tag_utils.get_revenue_value(event.event_tags) - value = event_tag_utils.get_numeric_value(event.event_tags, logger) + elif isinstance(event, user_event.ConversionEvent): + revenue = event_tag_utils.get_revenue_value(event.event_tags) + value = event_tag_utils.get_numeric_value(event.event_tags, logger) - snapshot_event = payload.SnapshotEvent( - event.event.id, - event.uuid, - event.event.key, - event.timestamp, - revenue, - value, - event.event_tags - ) + snapshot_event = payload.SnapshotEvent( + event.event.id, event.uuid, event.event.key, event.timestamp, revenue, value, event.event_tags, + ) - snapshot = payload.Snapshot([snapshot_event]) + snapshot = payload.Snapshot([snapshot_event]) - visitor = payload.Visitor([snapshot], event.visitor_attributes, event.user_id) + visitor = payload.Visitor([snapshot], event.visitor_attributes, event.user_id) - return visitor + return visitor - else: - logger.error('Invalid user event.') - return None + else: + logger.error('Invalid user event.') + return None - @staticmethod - def build_attribute_list(attributes, project_config): - """ Create Vistor Attribute List. + @staticmethod + def build_attribute_list(attributes, project_config): + """ Create Vistor Attribute List. Args: attributes: Dict representing user attributes and values which need to be recorded or None. @@ -144,35 +131,34 @@ def build_attribute_list(attributes, project_config): List consisting of valid attributes for the user. Empty otherwise. """ - attributes_list = [] - - if project_config is None: - return attributes_list - - if isinstance(attributes, dict): - for attribute_key in attributes.keys(): - attribute_value = attributes.get(attribute_key) - # Omit attribute values that are not supported by the log endpoint. - if validator.is_attribute_valid(attribute_key, attribute_value): - attribute_id = project_config.get_attribute_id(attribute_key) - if attribute_id: + attributes_list = [] + + if project_config is None: + return attributes_list + + if isinstance(attributes, dict): + for attribute_key in attributes.keys(): + attribute_value = attributes.get(attribute_key) + # Omit attribute values that are not supported by the log endpoint. + if validator.is_attribute_valid(attribute_key, attribute_value): + attribute_id = project_config.get_attribute_id(attribute_key) + if attribute_id: + attributes_list.append( + payload.VisitorAttribute( + attribute_id, attribute_key, CUSTOM_ATTRIBUTE_FEATURE_TYPE, attribute_value, + ) + ) + + # Append Bot Filtering Attribute + bot_filtering_value = project_config.get_bot_filtering_value() + if isinstance(bot_filtering_value, bool): attributes_list.append( - payload.VisitorAttribute( - attribute_id, - attribute_key, - CUSTOM_ATTRIBUTE_FEATURE_TYPE, - attribute_value) + payload.VisitorAttribute( + enums.ControlAttributes.BOT_FILTERING, + enums.ControlAttributes.BOT_FILTERING, + CUSTOM_ATTRIBUTE_FEATURE_TYPE, + bot_filtering_value, + ) ) - # Append Bot Filtering Attribute - bot_filtering_value = project_config.get_bot_filtering_value() - if isinstance(bot_filtering_value, bool): - attributes_list.append( - payload.VisitorAttribute( - enums.ControlAttributes.BOT_FILTERING, - enums.ControlAttributes.BOT_FILTERING, - CUSTOM_ATTRIBUTE_FEATURE_TYPE, - bot_filtering_value) - ) - - return attributes_list + return attributes_list diff --git a/optimizely/event/event_processor.py b/optimizely/event/event_processor.py index 6f3f4862..db44c041 100644 --- a/optimizely/event/event_processor.py +++ b/optimizely/event/event_processor.py @@ -31,19 +31,19 @@ class BaseEventProcessor(ABC): - """ Class encapsulating event processing. Override with your own implementation. """ + """ Class encapsulating event processing. Override with your own implementation. """ - @abc.abstractmethod - def process(self, user_event): - """ Method to provide intermediary processing stage within event production. + @abc.abstractmethod + def process(self, user_event): + """ Method to provide intermediary processing stage within event production. Args: user_event: UserEvent instance that needs to be processed and dispatched. """ - pass + pass class BatchEventProcessor(BaseEventProcessor): - """ + """ BatchEventProcessor is an implementation of the BaseEventProcessor that batches events. The BatchEventProcessor maintains a single consumer thread that pulls events off of @@ -51,24 +51,26 @@ class BatchEventProcessor(BaseEventProcessor): maximum duration before the resulting LogEvent is sent to the EventDispatcher. """ - _DEFAULT_QUEUE_CAPACITY = 1000 - _DEFAULT_BATCH_SIZE = 10 - _DEFAULT_FLUSH_INTERVAL = 30 - _DEFAULT_TIMEOUT_INTERVAL = 5 - _SHUTDOWN_SIGNAL = object() - _FLUSH_SIGNAL = object() - LOCK = threading.Lock() - - def __init__(self, - event_dispatcher, - logger=None, - start_on_init=False, - event_queue=None, - batch_size=None, - flush_interval=None, - timeout_interval=None, - notification_center=None): - """ BatchEventProcessor init method to configure event batching. + _DEFAULT_QUEUE_CAPACITY = 1000 + _DEFAULT_BATCH_SIZE = 10 + _DEFAULT_FLUSH_INTERVAL = 30 + _DEFAULT_TIMEOUT_INTERVAL = 5 + _SHUTDOWN_SIGNAL = object() + _FLUSH_SIGNAL = object() + LOCK = threading.Lock() + + def __init__( + self, + event_dispatcher, + logger=None, + start_on_init=False, + event_queue=None, + batch_size=None, + flush_interval=None, + timeout_interval=None, + notification_center=None, + ): + """ BatchEventProcessor init method to configure event batching. Args: event_dispatcher: Provides a dispatch_event method which if given a URL and params sends a request to it. @@ -84,43 +86,44 @@ def __init__(self, thread. notification_center: Optional instance of notification_center.NotificationCenter. """ - self.event_dispatcher = event_dispatcher or default_event_dispatcher - self.logger = _logging.adapt_logger(logger or _logging.NoOpLogger()) - self.event_queue = event_queue or queue.Queue(maxsize=self._DEFAULT_QUEUE_CAPACITY) - self.batch_size = batch_size if self._validate_instantiation_props(batch_size, - 'batch_size', - self._DEFAULT_BATCH_SIZE) \ - else self._DEFAULT_BATCH_SIZE - self.flush_interval = timedelta(seconds=flush_interval) \ - if self._validate_instantiation_props(flush_interval, - 'flush_interval', - self._DEFAULT_FLUSH_INTERVAL) \ - else timedelta(seconds=self._DEFAULT_FLUSH_INTERVAL) - self.timeout_interval = timedelta(seconds=timeout_interval) \ - if self._validate_instantiation_props(timeout_interval, - 'timeout_interval', - self._DEFAULT_TIMEOUT_INTERVAL) \ - else timedelta(seconds=self._DEFAULT_TIMEOUT_INTERVAL) - - self.notification_center = notification_center or _notification_center.NotificationCenter(self.logger) - self._current_batch = list() - - if not validator.is_notification_center_valid(self.notification_center): - self.logger.error(enums.Errors.INVALID_INPUT.format('notification_center')) - self.logger.debug('Creating notification center for use.') - self.notification_center = _notification_center.NotificationCenter(self.logger) - - self.executor = None - if start_on_init is True: - self.start() - - @property - def is_running(self): - """ Property to check if consumer thread is alive or not. """ - return self.executor.isAlive() if self.executor else False - - def _validate_instantiation_props(self, prop, prop_name, default_value): - """ Method to determine if instantiation properties like batch_size, flush_interval + self.event_dispatcher = event_dispatcher or default_event_dispatcher + self.logger = _logging.adapt_logger(logger or _logging.NoOpLogger()) + self.event_queue = event_queue or queue.Queue(maxsize=self._DEFAULT_QUEUE_CAPACITY) + self.batch_size = ( + batch_size + if self._validate_instantiation_props(batch_size, 'batch_size', self._DEFAULT_BATCH_SIZE) + else self._DEFAULT_BATCH_SIZE + ) + self.flush_interval = ( + timedelta(seconds=flush_interval) + if self._validate_instantiation_props(flush_interval, 'flush_interval', self._DEFAULT_FLUSH_INTERVAL) + else timedelta(seconds=self._DEFAULT_FLUSH_INTERVAL) + ) + self.timeout_interval = ( + timedelta(seconds=timeout_interval) + if self._validate_instantiation_props(timeout_interval, 'timeout_interval', self._DEFAULT_TIMEOUT_INTERVAL) + else timedelta(seconds=self._DEFAULT_TIMEOUT_INTERVAL) + ) + + self.notification_center = notification_center or _notification_center.NotificationCenter(self.logger) + self._current_batch = list() + + if not validator.is_notification_center_valid(self.notification_center): + self.logger.error(enums.Errors.INVALID_INPUT.format('notification_center')) + self.logger.debug('Creating notification center for use.') + self.notification_center = _notification_center.NotificationCenter(self.logger) + + self.executor = None + if start_on_init is True: + self.start() + + @property + def is_running(self): + """ Property to check if consumer thread is alive or not. """ + return self.executor.isAlive() if self.executor else False + + def _validate_instantiation_props(self, prop, prop_name, default_value): + """ Method to determine if instantiation properties like batch_size, flush_interval and timeout_interval are valid. Args: @@ -133,21 +136,21 @@ def _validate_instantiation_props(self, prop, prop_name, default_value): False if property name is batch_size and value is a floating point number. True otherwise. """ - is_valid = True + is_valid = True - if prop is None or not validator.is_finite_number(prop) or prop <= 0: - is_valid = False + if prop is None or not validator.is_finite_number(prop) or prop <= 0: + is_valid = False - if prop_name == 'batch_size' and not isinstance(prop, numbers.Integral): - is_valid = False + if prop_name == 'batch_size' and not isinstance(prop, numbers.Integral): + is_valid = False - if is_valid is False: - self.logger.info('Using default value {} for {}.'.format(default_value, prop_name)) + if is_valid is False: + self.logger.info('Using default value {} for {}.'.format(default_value, prop_name)) - return is_valid + return is_valid - def _get_time(self, _time=None): - """ Method to return rounded off time as integer in seconds. If _time is None, uses current time. + def _get_time(self, _time=None): + """ Method to return rounded off time as integer in seconds. If _time is None, uses current time. Args: _time: time in seconds that needs to be rounded off. @@ -155,125 +158,123 @@ def _get_time(self, _time=None): Returns: Integer time in seconds. """ - if _time is None: - return int(round(time.time())) + if _time is None: + return int(round(time.time())) - return int(round(_time)) + return int(round(_time)) - def start(self): - """ Starts the batch processing thread to batch events. """ - if hasattr(self, 'executor') and self.is_running: - self.logger.warning('BatchEventProcessor already started.') - return + def start(self): + """ Starts the batch processing thread to batch events. """ + if hasattr(self, 'executor') and self.is_running: + self.logger.warning('BatchEventProcessor already started.') + return - self.flushing_interval_deadline = self._get_time() + self._get_time(self.flush_interval.total_seconds()) - self.executor = threading.Thread(target=self._run) - self.executor.setDaemon(True) - self.executor.start() + self.flushing_interval_deadline = self._get_time() + self._get_time(self.flush_interval.total_seconds()) + self.executor = threading.Thread(target=self._run) + self.executor.setDaemon(True) + self.executor.start() - def _run(self): - """ Triggered as part of the thread which batches events or flushes event_queue and sleeps + def _run(self): + """ Triggered as part of the thread which batches events or flushes event_queue and sleeps periodically if queue is empty. """ - try: - while True: - if self._get_time() >= self.flushing_interval_deadline: - self._flush_queue() - try: - item = self.event_queue.get(False) + while True: + if self._get_time() >= self.flushing_interval_deadline: + self._flush_queue() - except queue.Empty: - time.sleep(0.05) - continue + try: + item = self.event_queue.get(False) - if item == self._SHUTDOWN_SIGNAL: - self.logger.debug('Received shutdown signal.') - break + except queue.Empty: + time.sleep(0.05) + continue - if item == self._FLUSH_SIGNAL: - self.logger.debug('Received flush signal.') - self._flush_queue() - continue + if item == self._SHUTDOWN_SIGNAL: + self.logger.debug('Received shutdown signal.') + break - if isinstance(item, UserEvent): - self._add_to_batch(item) + if item == self._FLUSH_SIGNAL: + self.logger.debug('Received flush signal.') + self._flush_queue() + continue - except Exception as exception: - self.logger.error('Uncaught exception processing buffer. Error: ' + str(exception)) + if isinstance(item, UserEvent): + self._add_to_batch(item) - finally: - self.logger.info('Exiting processing loop. Attempting to flush pending events.') - self._flush_queue() + except Exception as exception: + self.logger.error('Uncaught exception processing buffer. Error: ' + str(exception)) - def flush(self): - """ Adds flush signal to event_queue. """ + finally: + self.logger.info('Exiting processing loop. Attempting to flush pending events.') + self._flush_queue() - self.event_queue.put(self._FLUSH_SIGNAL) + def flush(self): + """ Adds flush signal to event_queue. """ - def _flush_queue(self): - """ Flushes event_queue by dispatching events. """ + self.event_queue.put(self._FLUSH_SIGNAL) - if len(self._current_batch) == 0: - return + def _flush_queue(self): + """ Flushes event_queue by dispatching events. """ - with self.LOCK: - to_process_batch = list(self._current_batch) - self._current_batch = list() + if len(self._current_batch) == 0: + return - log_event = EventFactory.create_log_event(to_process_batch, self.logger) + with self.LOCK: + to_process_batch = list(self._current_batch) + self._current_batch = list() - self.notification_center.send_notifications( - enums.NotificationTypes.LOG_EVENT, - log_event - ) + log_event = EventFactory.create_log_event(to_process_batch, self.logger) - try: - self.event_dispatcher.dispatch_event(log_event) - except Exception as e: - self.logger.error('Error dispatching event: ' + str(log_event) + ' ' + str(e)) + self.notification_center.send_notifications(enums.NotificationTypes.LOG_EVENT, log_event) + + try: + self.event_dispatcher.dispatch_event(log_event) + except Exception as e: + self.logger.error('Error dispatching event: ' + str(log_event) + ' ' + str(e)) - def process(self, user_event): - """ Method to process the user_event by putting it in event_queue. + def process(self, user_event): + """ Method to process the user_event by putting it in event_queue. Args: user_event: UserEvent Instance. """ - if not isinstance(user_event, UserEvent): - self.logger.error('Provided event is in an invalid format.') - return + if not isinstance(user_event, UserEvent): + self.logger.error('Provided event is in an invalid format.') + return - self.logger.debug('Received event of type {} for user {}.'.format( - type(user_event).__name__, user_event.user_id) - ) + self.logger.debug( + 'Received event of type {} for user {}.'.format(type(user_event).__name__, user_event.user_id) + ) - try: - self.event_queue.put_nowait(user_event) - except queue.Full: - self.logger.debug('Payload not accepted by the queue. Current size: {}'.format(str(self.event_queue.qsize()))) + try: + self.event_queue.put_nowait(user_event) + except queue.Full: + self.logger.debug( + 'Payload not accepted by the queue. Current size: {}'.format(str(self.event_queue.qsize())) + ) - def _add_to_batch(self, user_event): - """ Method to append received user event to current batch. + def _add_to_batch(self, user_event): + """ Method to append received user event to current batch. Args: user_event: UserEvent Instance. """ - if self._should_split(user_event): - self._flush_queue() - self._current_batch = list() + if self._should_split(user_event): + self._flush_queue() + self._current_batch = list() - # Reset the deadline if starting a new batch. - if len(self._current_batch) == 0: - self.flushing_interval_deadline = self._get_time() + \ - self._get_time(self.flush_interval.total_seconds()) + # Reset the deadline if starting a new batch. + if len(self._current_batch) == 0: + self.flushing_interval_deadline = self._get_time() + self._get_time(self.flush_interval.total_seconds()) - with self.LOCK: - self._current_batch.append(user_event) - if len(self._current_batch) >= self.batch_size: - self._flush_queue() + with self.LOCK: + self._current_batch.append(user_event) + if len(self._current_batch) >= self.batch_size: + self._flush_queue() - def _should_split(self, user_event): - """ Method to check if current event batch should split into two. + def _should_split(self, user_event): + """ Method to check if current event batch should split into two. Args: user_event: UserEvent Instance. @@ -283,77 +284,74 @@ def _should_split(self, user_event): revision number and project id respectively. - False, otherwise. """ - if len(self._current_batch) == 0: - return False + if len(self._current_batch) == 0: + return False - current_context = self._current_batch[-1].event_context - new_context = user_event.event_context + current_context = self._current_batch[-1].event_context + new_context = user_event.event_context - if current_context.revision != new_context.revision: - return True + if current_context.revision != new_context.revision: + return True - if current_context.project_id != new_context.project_id: - return True + if current_context.project_id != new_context.project_id: + return True - return False + return False - def stop(self): - """ Stops and disposes batch event processor. """ - self.event_queue.put(self._SHUTDOWN_SIGNAL) - self.logger.warning('Stopping Scheduler.') + def stop(self): + """ Stops and disposes batch event processor. """ + self.event_queue.put(self._SHUTDOWN_SIGNAL) + self.logger.warning('Stopping Scheduler.') - if self.executor: - self.executor.join(self.timeout_interval.total_seconds()) + if self.executor: + self.executor.join(self.timeout_interval.total_seconds()) - if self.is_running: - self.logger.error('Timeout exceeded while attempting to close for ' + str(self.timeout_interval) + ' ms.') + if self.is_running: + self.logger.error('Timeout exceeded while attempting to close for ' + str(self.timeout_interval) + ' ms.') class ForwardingEventProcessor(BaseEventProcessor): - """ + """ ForwardingEventProcessor serves as the default EventProcessor. The ForwardingEventProcessor sends the LogEvent to EventDispatcher as soon as it is received. """ - def __init__(self, event_dispatcher, logger=None, notification_center=None): - """ ForwardingEventProcessor init method to configure event dispatching. + def __init__(self, event_dispatcher, logger=None, notification_center=None): + """ ForwardingEventProcessor init method to configure event dispatching. Args: 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. notification_center: Optional instance of notification_center.NotificationCenter. """ - self.event_dispatcher = event_dispatcher - self.logger = _logging.adapt_logger(logger or _logging.NoOpLogger()) - self.notification_center = notification_center or _notification_center.NotificationCenter(self.logger) + self.event_dispatcher = event_dispatcher + self.logger = _logging.adapt_logger(logger or _logging.NoOpLogger()) + self.notification_center = notification_center or _notification_center.NotificationCenter(self.logger) - if not validator.is_notification_center_valid(self.notification_center): - self.logger.error(enums.Errors.INVALID_INPUT.format('notification_center')) - self.notification_center = _notification_center.NotificationCenter() + if not validator.is_notification_center_valid(self.notification_center): + self.logger.error(enums.Errors.INVALID_INPUT.format('notification_center')) + self.notification_center = _notification_center.NotificationCenter() - def process(self, user_event): - """ Method to process the user_event by dispatching it. + def process(self, user_event): + """ Method to process the user_event by dispatching it. Args: user_event: UserEvent Instance. """ - if not isinstance(user_event, UserEvent): - self.logger.error('Provided event is in an invalid format.') - return + if not isinstance(user_event, UserEvent): + self.logger.error('Provided event is in an invalid format.') + return - self.logger.debug('Received event of type {} for user {}.'.format( - type(user_event).__name__, user_event.user_id) - ) + self.logger.debug( + 'Received event of type {} for user {}.'.format(type(user_event).__name__, user_event.user_id) + ) - log_event = EventFactory.create_log_event(user_event, self.logger) + log_event = EventFactory.create_log_event(user_event, self.logger) - self.notification_center.send_notifications( - enums.NotificationTypes.LOG_EVENT, - log_event - ) + self.notification_center.send_notifications(enums.NotificationTypes.LOG_EVENT, log_event) - try: - self.event_dispatcher.dispatch_event(log_event) - except Exception as e: - self.logger.exception('Error dispatching event: ' + str(log_event) + ' ' + str(e)) + try: + self.event_dispatcher.dispatch_event(log_event) + except Exception as e: + self.logger.exception('Error dispatching event: ' + str(log_event) + ' ' + str(e)) diff --git a/optimizely/event/log_event.py b/optimizely/event/log_event.py index 30839faa..1c5ce71d 100644 --- a/optimizely/event/log_event.py +++ b/optimizely/event/log_event.py @@ -13,13 +13,13 @@ class LogEvent(object): - """ Representation of an event which can be sent to Optimizely events API. """ + """ Representation of an event which can be sent to Optimizely events API. """ - def __init__(self, url, params, http_verb=None, headers=None): - self.url = url - self.params = params - self.http_verb = http_verb or 'POST' - self.headers = headers + def __init__(self, url, params, http_verb=None, headers=None): + self.url = url + self.params = params + self.http_verb = http_verb or 'POST' + self.headers = headers - def __str__(self): - return str(self.__class__) + ": " + str(self.__dict__) + def __str__(self): + return str(self.__class__) + ": " + str(self.__dict__) diff --git a/optimizely/event/payload.py b/optimizely/event/payload.py index 0a1c34d4..450acd55 100644 --- a/optimizely/event/payload.py +++ b/optimizely/event/payload.py @@ -15,87 +15,93 @@ class EventBatch(object): - """ Class respresenting Event Batch. """ - - def __init__(self, account_id, project_id, revision, client_name, client_version, - anonymize_ip, enrich_decisions=True, visitors=None): - self.account_id = account_id - self.project_id = project_id - self.revision = revision - self.client_name = client_name - self.client_version = client_version - self.anonymize_ip = anonymize_ip - self.enrich_decisions = enrich_decisions - self.visitors = visitors or [] - - def __eq__(self, other): - batch_obj = self.get_event_params() - return batch_obj == other - - def _dict_clean(self, obj): - """ Helper method to remove keys from dictionary with None values. """ - - result = {} - for k, v in obj: - if v is None and k in ['revenue', 'value', 'tags', 'decisions']: - continue - else: - result[k] = v - return result - - def get_event_params(self): - """ Method to return valid params for LogEvent payload. """ - - return json.loads( - json.dumps(self.__dict__, default=lambda o: o.__dict__), - object_pairs_hook=self._dict_clean - ) + """ Class respresenting Event Batch. """ + + def __init__( + self, + account_id, + project_id, + revision, + client_name, + client_version, + anonymize_ip, + enrich_decisions=True, + visitors=None, + ): + self.account_id = account_id + self.project_id = project_id + self.revision = revision + self.client_name = client_name + self.client_version = client_version + self.anonymize_ip = anonymize_ip + self.enrich_decisions = enrich_decisions + self.visitors = visitors or [] + + def __eq__(self, other): + batch_obj = self.get_event_params() + return batch_obj == other + + def _dict_clean(self, obj): + """ Helper method to remove keys from dictionary with None values. """ + + result = {} + for k, v in obj: + if v is None and k in ['revenue', 'value', 'tags', 'decisions']: + continue + else: + result[k] = v + return result + + def get_event_params(self): + """ Method to return valid params for LogEvent payload. """ + + return json.loads(json.dumps(self.__dict__, default=lambda o: o.__dict__), object_pairs_hook=self._dict_clean,) class Decision(object): - """ Class respresenting Decision. """ + """ Class respresenting Decision. """ - def __init__(self, campaign_id, experiment_id, variation_id): - self.campaign_id = campaign_id - self.experiment_id = experiment_id - self.variation_id = variation_id + def __init__(self, campaign_id, experiment_id, variation_id): + self.campaign_id = campaign_id + self.experiment_id = experiment_id + self.variation_id = variation_id class Snapshot(object): - """ Class representing Snapshot. """ + """ Class representing Snapshot. """ - def __init__(self, events, decisions=None): - self.events = events - self.decisions = decisions + def __init__(self, events, decisions=None): + self.events = events + self.decisions = decisions class SnapshotEvent(object): - """ Class representing Snapshot Event. """ + """ Class representing Snapshot Event. """ - def __init__(self, entity_id, uuid, key, timestamp, revenue=None, value=None, tags=None): - self.entity_id = entity_id - self.uuid = uuid - self.key = key - self.timestamp = timestamp - self.revenue = revenue - self.value = value - self.tags = tags + def __init__(self, entity_id, uuid, key, timestamp, revenue=None, value=None, tags=None): + self.entity_id = entity_id + self.uuid = uuid + self.key = key + self.timestamp = timestamp + self.revenue = revenue + self.value = value + self.tags = tags class Visitor(object): - """ Class representing Visitor. """ + """ Class representing Visitor. """ - def __init__(self, snapshots, attributes, visitor_id): - self.snapshots = snapshots - self.attributes = attributes - self.visitor_id = visitor_id + def __init__(self, snapshots, attributes, visitor_id): + self.snapshots = snapshots + self.attributes = attributes + self.visitor_id = visitor_id class VisitorAttribute(object): - """ Class representing Visitor Attribute. """ + """ Class representing Visitor Attribute. """ - def __init__(self, entity_id, key, attribute_type, value): - self.entity_id = entity_id - self.key = key - self.type = attribute_type - self.value = value + def __init__(self, entity_id, key, attribute_type, value): + self.entity_id = entity_id + self.key = key + self.type = attribute_type + self.value = value diff --git a/optimizely/event/user_event.py b/optimizely/event/user_event.py index e64e6989..6eb014f9 100644 --- a/optimizely/event/user_event.py +++ b/optimizely/event/user_event.py @@ -20,48 +20,52 @@ class UserEvent(object): - """ Class respresenting User Event. """ + """ Class respresenting User Event. """ - def __init__(self, event_context, user_id, visitor_attributes, bot_filtering=None): - self.event_context = event_context - self.user_id = user_id - self.visitor_attributes = visitor_attributes - self.bot_filtering = bot_filtering - self.uuid = self._get_uuid() - self.timestamp = self._get_time() + def __init__(self, event_context, user_id, visitor_attributes, bot_filtering=None): + self.event_context = event_context + self.user_id = user_id + self.visitor_attributes = visitor_attributes + self.bot_filtering = bot_filtering + self.uuid = self._get_uuid() + self.timestamp = self._get_time() - def _get_time(self): - return int(round(time.time() * 1000)) + def _get_time(self): + return int(round(time.time() * 1000)) - def _get_uuid(self): - return str(uuid.uuid4()) + def _get_uuid(self): + return str(uuid.uuid4()) class ImpressionEvent(UserEvent): - """ Class representing Impression Event. """ + """ Class representing Impression Event. """ - def __init__(self, event_context, user_id, experiment, visitor_attributes, variation, bot_filtering=None): - super(ImpressionEvent, self).__init__(event_context, user_id, visitor_attributes, bot_filtering) - self.experiment = experiment - self.variation = variation + def __init__( + self, event_context, user_id, experiment, visitor_attributes, variation, bot_filtering=None, + ): + super(ImpressionEvent, self).__init__(event_context, user_id, visitor_attributes, bot_filtering) + self.experiment = experiment + self.variation = variation class ConversionEvent(UserEvent): - """ Class representing Conversion Event. """ + """ Class representing Conversion Event. """ - def __init__(self, event_context, event, user_id, visitor_attributes, event_tags, bot_filtering=None): - super(ConversionEvent, self).__init__(event_context, user_id, visitor_attributes, bot_filtering) - self.event = event - self.event_tags = event_tags + def __init__( + self, event_context, event, user_id, visitor_attributes, event_tags, bot_filtering=None, + ): + super(ConversionEvent, self).__init__(event_context, user_id, visitor_attributes, bot_filtering) + self.event = event + self.event_tags = event_tags class EventContext(object): - """ Class respresenting User Event Context. """ - - def __init__(self, account_id, project_id, revision, anonymize_ip): - self.account_id = account_id - self.project_id = project_id - self.revision = revision - self.client_name = CLIENT_NAME - self.client_version = version.__version__ - self.anonymize_ip = anonymize_ip + """ Class respresenting User Event Context. """ + + def __init__(self, account_id, project_id, revision, anonymize_ip): + self.account_id = account_id + self.project_id = project_id + self.revision = revision + self.client_name = CLIENT_NAME + self.client_version = version.__version__ + self.anonymize_ip = anonymize_ip diff --git a/optimizely/event/user_event_factory.py b/optimizely/event/user_event_factory.py index 9699c570..15908cc7 100644 --- a/optimizely/event/user_event_factory.py +++ b/optimizely/event/user_event_factory.py @@ -16,11 +16,13 @@ class UserEventFactory(object): - """ UserEventFactory builds impression and conversion events from a given UserEvent. """ + """ UserEventFactory builds impression and conversion events from a given UserEvent. """ - @classmethod - def create_impression_event(cls, project_config, activated_experiment, variation_id, user_id, user_attributes): - """ Create impression Event to be sent to the logging endpoint. + @classmethod + def create_impression_event( + cls, project_config, activated_experiment, variation_id, user_id, user_attributes, + ): + """ Create impression Event to be sent to the logging endpoint. Args: project_config: Instance of ProjectConfig. @@ -34,31 +36,28 @@ def create_impression_event(cls, project_config, activated_experiment, variation - activated_experiment is None. """ - if not activated_experiment: - return None + if not activated_experiment: + return None - experiment_key = activated_experiment.key - variation = project_config.get_variation_from_id(experiment_key, variation_id) + experiment_key = activated_experiment.key + variation = project_config.get_variation_from_id(experiment_key, variation_id) - event_context = user_event.EventContext( - project_config.account_id, - project_config.project_id, - project_config.revision, - project_config.anonymize_ip - ) + event_context = user_event.EventContext( + project_config.account_id, project_config.project_id, project_config.revision, project_config.anonymize_ip, + ) - return user_event.ImpressionEvent( - event_context, - user_id, - activated_experiment, - event_factory.EventFactory.build_attribute_list(user_attributes, project_config), - variation, - project_config.get_bot_filtering_value() - ) + return user_event.ImpressionEvent( + event_context, + user_id, + activated_experiment, + event_factory.EventFactory.build_attribute_list(user_attributes, project_config), + variation, + project_config.get_bot_filtering_value(), + ) - @classmethod - def create_conversion_event(cls, project_config, event_key, user_id, user_attributes, event_tags): - """ Create conversion Event to be sent to the logging endpoint. + @classmethod + def create_conversion_event(cls, project_config, event_key, user_id, user_attributes, event_tags): + """ Create conversion Event to be sent to the logging endpoint. Args: project_config: Instance of ProjectConfig. @@ -71,18 +70,15 @@ def create_conversion_event(cls, project_config, event_key, user_id, user_attrib Event object encapsulating the conversion event. """ - event_context = user_event.EventContext( - project_config.account_id, - project_config.project_id, - project_config.revision, - project_config.anonymize_ip - ) + event_context = user_event.EventContext( + project_config.account_id, project_config.project_id, project_config.revision, project_config.anonymize_ip, + ) - return user_event.ConversionEvent( - event_context, - project_config.get_event(event_key), - user_id, - event_factory.EventFactory.build_attribute_list(user_attributes, project_config), - event_tags, - project_config.get_bot_filtering_value() - ) + return user_event.ConversionEvent( + event_context, + project_config.get_event(event_key), + user_id, + event_factory.EventFactory.build_attribute_list(user_attributes, project_config), + event_tags, + project_config.get_bot_filtering_value(), + ) diff --git a/optimizely/event_builder.py b/optimizely/event_builder.py index 293bcea1..befe2700 100644 --- a/optimizely/event_builder.py +++ b/optimizely/event_builder.py @@ -21,49 +21,49 @@ class Event(object): - """ Representation of an event which can be sent to the Optimizely logging endpoint. """ + """ Representation of an event which can be sent to the Optimizely logging endpoint. """ - def __init__(self, url, params, http_verb=None, headers=None): - self.url = url - self.params = params - self.http_verb = http_verb or 'GET' - self.headers = headers + def __init__(self, url, params, http_verb=None, headers=None): + self.url = url + self.params = params + self.http_verb = http_verb or 'GET' + self.headers = headers class EventBuilder(object): - """ Class which encapsulates methods to build events for tracking + """ Class which encapsulates methods to build events for tracking impressions and conversions using the new V3 event API (batch). """ - EVENTS_URL = 'https://logx.optimizely.com/v1/events' - HTTP_VERB = 'POST' - HTTP_HEADERS = {'Content-Type': 'application/json'} - - class EventParams(object): - ACCOUNT_ID = 'account_id' - PROJECT_ID = 'project_id' - EXPERIMENT_ID = 'experiment_id' - CAMPAIGN_ID = 'campaign_id' - VARIATION_ID = 'variation_id' - END_USER_ID = 'visitor_id' - ENRICH_DECISIONS = 'enrich_decisions' - EVENTS = 'events' - EVENT_ID = 'entity_id' - ATTRIBUTES = 'attributes' - DECISIONS = 'decisions' - TIME = 'timestamp' - KEY = 'key' - TAGS = 'tags' - UUID = 'uuid' - USERS = 'visitors' - SNAPSHOTS = 'snapshots' - SOURCE_SDK_TYPE = 'client_name' - SOURCE_SDK_VERSION = 'client_version' - CUSTOM = 'custom' - ANONYMIZE_IP = 'anonymize_ip' - REVISION = 'revision' - - def _get_attributes_data(self, project_config, attributes): - """ Get attribute(s) information. + EVENTS_URL = 'https://logx.optimizely.com/v1/events' + HTTP_VERB = 'POST' + HTTP_HEADERS = {'Content-Type': 'application/json'} + + class EventParams(object): + ACCOUNT_ID = 'account_id' + PROJECT_ID = 'project_id' + EXPERIMENT_ID = 'experiment_id' + CAMPAIGN_ID = 'campaign_id' + VARIATION_ID = 'variation_id' + END_USER_ID = 'visitor_id' + ENRICH_DECISIONS = 'enrich_decisions' + EVENTS = 'events' + EVENT_ID = 'entity_id' + ATTRIBUTES = 'attributes' + DECISIONS = 'decisions' + TIME = 'timestamp' + KEY = 'key' + TAGS = 'tags' + UUID = 'uuid' + USERS = 'visitors' + SNAPSHOTS = 'snapshots' + SOURCE_SDK_TYPE = 'client_name' + SOURCE_SDK_VERSION = 'client_version' + CUSTOM = 'custom' + ANONYMIZE_IP = 'anonymize_ip' + REVISION = 'revision' + + def _get_attributes_data(self, project_config, attributes): + """ Get attribute(s) information. Args: project_config: Instance of ProjectConfig. @@ -73,45 +73,49 @@ def _get_attributes_data(self, project_config, attributes): List consisting of valid attributes for the user. Empty otherwise. """ - params = [] - - if isinstance(attributes, dict): - for attribute_key in attributes.keys(): - attribute_value = attributes.get(attribute_key) - # Omit attribute values that are not supported by the log endpoint. - if validator.is_attribute_valid(attribute_key, attribute_value): - attribute_id = project_config.get_attribute_id(attribute_key) - if attribute_id: - params.append({ - 'entity_id': attribute_id, - 'key': attribute_key, - 'type': self.EventParams.CUSTOM, - 'value': attribute_value - }) - - # Append Bot Filtering Attribute - bot_filtering_value = project_config.get_bot_filtering_value() - if isinstance(bot_filtering_value, bool): - params.append({ - 'entity_id': enums.ControlAttributes.BOT_FILTERING, - 'key': enums.ControlAttributes.BOT_FILTERING, - 'type': self.EventParams.CUSTOM, - 'value': bot_filtering_value - }) - - return params - - def _get_time(self): - """ Get time in milliseconds to be added. + params = [] + + if isinstance(attributes, dict): + for attribute_key in attributes.keys(): + attribute_value = attributes.get(attribute_key) + # Omit attribute values that are not supported by the log endpoint. + if validator.is_attribute_valid(attribute_key, attribute_value): + attribute_id = project_config.get_attribute_id(attribute_key) + if attribute_id: + params.append( + { + 'entity_id': attribute_id, + 'key': attribute_key, + 'type': self.EventParams.CUSTOM, + 'value': attribute_value, + } + ) + + # Append Bot Filtering Attribute + bot_filtering_value = project_config.get_bot_filtering_value() + if isinstance(bot_filtering_value, bool): + params.append( + { + 'entity_id': enums.ControlAttributes.BOT_FILTERING, + 'key': enums.ControlAttributes.BOT_FILTERING, + 'type': self.EventParams.CUSTOM, + 'value': bot_filtering_value, + } + ) + + return params + + def _get_time(self): + """ Get time in milliseconds to be added. Returns: int Current time in milliseconds. """ - return int(round(time.time() * 1000)) + return int(round(time.time() * 1000)) - def _get_common_params(self, project_config, user_id, attributes): - """ Get params which are used same in both conversion and impression events. + def _get_common_params(self, project_config, user_id, attributes): + """ Get params which are used same in both conversion and impression events. Args: project_config: Instance of ProjectConfig. @@ -121,32 +125,32 @@ def _get_common_params(self, project_config, user_id, attributes): Returns: Dict consisting of parameters common to both impression and conversion events. """ - common_params = { - self.EventParams.PROJECT_ID: project_config.get_project_id(), - self.EventParams.ACCOUNT_ID: project_config.get_account_id() - } + common_params = { + self.EventParams.PROJECT_ID: project_config.get_project_id(), + self.EventParams.ACCOUNT_ID: project_config.get_account_id(), + } - visitor = { - self.EventParams.END_USER_ID: user_id, - self.EventParams.SNAPSHOTS: [] - } + visitor = { + self.EventParams.END_USER_ID: user_id, + self.EventParams.SNAPSHOTS: [], + } - common_params[self.EventParams.USERS] = [] - common_params[self.EventParams.USERS].append(visitor) - common_params[self.EventParams.USERS][0][self.EventParams.ATTRIBUTES] = self._get_attributes_data( - project_config, attributes - ) + common_params[self.EventParams.USERS] = [] + common_params[self.EventParams.USERS].append(visitor) + common_params[self.EventParams.USERS][0][self.EventParams.ATTRIBUTES] = self._get_attributes_data( + project_config, attributes + ) - common_params[self.EventParams.SOURCE_SDK_TYPE] = 'python-sdk' - common_params[self.EventParams.ENRICH_DECISIONS] = True - common_params[self.EventParams.SOURCE_SDK_VERSION] = version.__version__ - common_params[self.EventParams.ANONYMIZE_IP] = project_config.get_anonymize_ip_value() - common_params[self.EventParams.REVISION] = project_config.get_revision() + common_params[self.EventParams.SOURCE_SDK_TYPE] = 'python-sdk' + common_params[self.EventParams.ENRICH_DECISIONS] = True + common_params[self.EventParams.SOURCE_SDK_VERSION] = version.__version__ + common_params[self.EventParams.ANONYMIZE_IP] = project_config.get_anonymize_ip_value() + common_params[self.EventParams.REVISION] = project_config.get_revision() - return common_params + return common_params - def _get_required_params_for_impression(self, experiment, variation_id): - """ Get parameters that are required for the impression event to register. + def _get_required_params_for_impression(self, experiment, variation_id): + """ Get parameters that are required for the impression event to register. Args: experiment: Experiment for which impression needs to be recorded. @@ -155,25 +159,29 @@ def _get_required_params_for_impression(self, experiment, variation_id): Returns: Dict consisting of decisions and events info for impression event. """ - snapshot = {} - - snapshot[self.EventParams.DECISIONS] = [{ - self.EventParams.EXPERIMENT_ID: experiment.id, - self.EventParams.VARIATION_ID: variation_id, - self.EventParams.CAMPAIGN_ID: experiment.layerId - }] - - snapshot[self.EventParams.EVENTS] = [{ - self.EventParams.EVENT_ID: experiment.layerId, - self.EventParams.TIME: self._get_time(), - self.EventParams.KEY: 'campaign_activated', - self.EventParams.UUID: str(uuid.uuid4()) - }] - - return snapshot - - def _get_required_params_for_conversion(self, project_config, event_key, event_tags): - """ Get parameters that are required for the conversion event to register. + snapshot = {} + + snapshot[self.EventParams.DECISIONS] = [ + { + self.EventParams.EXPERIMENT_ID: experiment.id, + self.EventParams.VARIATION_ID: variation_id, + self.EventParams.CAMPAIGN_ID: experiment.layerId, + } + ] + + snapshot[self.EventParams.EVENTS] = [ + { + self.EventParams.EVENT_ID: experiment.layerId, + self.EventParams.TIME: self._get_time(), + self.EventParams.KEY: 'campaign_activated', + self.EventParams.UUID: str(uuid.uuid4()), + } + ] + + return snapshot + + def _get_required_params_for_conversion(self, project_config, event_key, event_tags): + """ Get parameters that are required for the conversion event to register. Args: project_config: Instance of ProjectConfig. @@ -183,32 +191,32 @@ def _get_required_params_for_conversion(self, project_config, event_key, event_t Returns: Dict consisting of the decisions and events info for conversion event. """ - snapshot = {} + snapshot = {} - event_dict = { - self.EventParams.EVENT_ID: project_config.get_event(event_key).id, - self.EventParams.TIME: self._get_time(), - self.EventParams.KEY: event_key, - self.EventParams.UUID: str(uuid.uuid4()) - } + event_dict = { + self.EventParams.EVENT_ID: project_config.get_event(event_key).id, + self.EventParams.TIME: self._get_time(), + self.EventParams.KEY: event_key, + self.EventParams.UUID: str(uuid.uuid4()), + } - if event_tags: - revenue_value = event_tag_utils.get_revenue_value(event_tags) - if revenue_value is not None: - event_dict[event_tag_utils.REVENUE_METRIC_TYPE] = revenue_value + if event_tags: + revenue_value = event_tag_utils.get_revenue_value(event_tags) + if revenue_value is not None: + event_dict[event_tag_utils.REVENUE_METRIC_TYPE] = revenue_value - numeric_value = event_tag_utils.get_numeric_value(event_tags, project_config.logger) - if numeric_value is not None: - event_dict[event_tag_utils.NUMERIC_METRIC_TYPE] = numeric_value + numeric_value = event_tag_utils.get_numeric_value(event_tags, project_config.logger) + if numeric_value is not None: + event_dict[event_tag_utils.NUMERIC_METRIC_TYPE] = numeric_value - if len(event_tags) > 0: - event_dict[self.EventParams.TAGS] = event_tags + if len(event_tags) > 0: + event_dict[self.EventParams.TAGS] = event_tags - snapshot[self.EventParams.EVENTS] = [event_dict] - return snapshot + snapshot[self.EventParams.EVENTS] = [event_dict] + return snapshot - def create_impression_event(self, project_config, experiment, variation_id, user_id, attributes): - """ Create impression Event to be sent to the logging endpoint. + def create_impression_event(self, project_config, experiment, variation_id, user_id, attributes): + """ Create impression Event to be sent to the logging endpoint. Args: project_config: Instance of ProjectConfig. @@ -221,18 +229,15 @@ def create_impression_event(self, project_config, experiment, variation_id, user Event object encapsulating the impression event. """ - params = self._get_common_params(project_config, user_id, attributes) - impression_params = self._get_required_params_for_impression(experiment, variation_id) + params = self._get_common_params(project_config, user_id, attributes) + impression_params = self._get_required_params_for_impression(experiment, variation_id) - params[self.EventParams.USERS][0][self.EventParams.SNAPSHOTS].append(impression_params) + params[self.EventParams.USERS][0][self.EventParams.SNAPSHOTS].append(impression_params) - return Event(self.EVENTS_URL, - params, - http_verb=self.HTTP_VERB, - headers=self.HTTP_HEADERS) + return Event(self.EVENTS_URL, params, http_verb=self.HTTP_VERB, headers=self.HTTP_HEADERS) - def create_conversion_event(self, project_config, event_key, user_id, attributes, event_tags): - """ Create conversion Event to be sent to the logging endpoint. + def create_conversion_event(self, project_config, event_key, user_id, attributes, event_tags): + """ Create conversion Event to be sent to the logging endpoint. Args: project_config: Instance of ProjectConfig. @@ -245,11 +250,8 @@ def create_conversion_event(self, project_config, event_key, user_id, attributes Event object encapsulating the conversion event. """ - params = self._get_common_params(project_config, user_id, attributes) - conversion_params = self._get_required_params_for_conversion(project_config, event_key, event_tags) + params = self._get_common_params(project_config, user_id, attributes) + conversion_params = self._get_required_params_for_conversion(project_config, event_key, event_tags) - params[self.EventParams.USERS][0][self.EventParams.SNAPSHOTS].append(conversion_params) - return Event(self.EVENTS_URL, - params, - http_verb=self.HTTP_VERB, - headers=self.HTTP_HEADERS) + params[self.EventParams.USERS][0][self.EventParams.SNAPSHOTS].append(conversion_params) + return Event(self.EVENTS_URL, params, http_verb=self.HTTP_VERB, headers=self.HTTP_HEADERS) diff --git a/optimizely/event_dispatcher.py b/optimizely/event_dispatcher.py index 247a3e0a..f21b47a1 100644 --- a/optimizely/event_dispatcher.py +++ b/optimizely/event_dispatcher.py @@ -23,22 +23,21 @@ class EventDispatcher(object): - - @staticmethod - def dispatch_event(event): - """ Dispatch the event being represented by the Event object. + @staticmethod + def dispatch_event(event): + """ Dispatch the event being represented by the Event object. Args: event: Object holding information about the request to be dispatched to the Optimizely backend. """ - try: - if event.http_verb == enums.HTTPVerbs.GET: - requests.get(event.url, params=event.params, timeout=REQUEST_TIMEOUT).raise_for_status() - elif event.http_verb == enums.HTTPVerbs.POST: - requests.post( - event.url, data=json.dumps(event.params), headers=event.headers, timeout=REQUEST_TIMEOUT - ).raise_for_status() + try: + if event.http_verb == enums.HTTPVerbs.GET: + requests.get(event.url, params=event.params, timeout=REQUEST_TIMEOUT).raise_for_status() + elif event.http_verb == enums.HTTPVerbs.POST: + requests.post( + event.url, data=json.dumps(event.params), headers=event.headers, timeout=REQUEST_TIMEOUT, + ).raise_for_status() - except request_exception.RequestException as error: - logging.error('Dispatch event failed. Error: %s' % str(error)) + except request_exception.RequestException as error: + logging.error('Dispatch event failed. Error: %s' % str(error)) diff --git a/optimizely/exceptions.py b/optimizely/exceptions.py index 1b027b1e..d6003ab1 100644 --- a/optimizely/exceptions.py +++ b/optimizely/exceptions.py @@ -13,45 +13,54 @@ class InvalidAttributeException(Exception): - """ Raised when provided attribute is invalid. """ - pass + """ Raised when provided attribute is invalid. """ + + pass class InvalidAudienceException(Exception): - """ Raised when provided audience is invalid. """ - pass + """ Raised when provided audience is invalid. """ + + pass class InvalidEventException(Exception): - """ Raised when provided event key is invalid. """ - pass + """ Raised when provided event key is invalid. """ + + pass class InvalidEventTagException(Exception): - """ Raised when provided event tag is invalid. """ - pass + """ Raised when provided event tag is invalid. """ + + pass class InvalidExperimentException(Exception): - """ Raised when provided experiment key is invalid. """ - pass + """ Raised when provided experiment key is invalid. """ + + pass class InvalidGroupException(Exception): - """ Raised when provided group ID is invalid. """ - pass + """ Raised when provided group ID is invalid. """ + + pass class InvalidInputException(Exception): - """ Raised when provided datafile, event dispatcher, logger, event processor or error handler is invalid. """ - pass + """ Raised when provided datafile, event dispatcher, logger, event processor or error handler is invalid. """ + + pass class InvalidVariationException(Exception): - """ Raised when provided variation is invalid. """ - pass + """ Raised when provided variation is invalid. """ + + pass class UnsupportedDatafileVersionException(Exception): - """ Raised when provided version in datafile is not supported. """ - pass + """ Raised when provided version in datafile is not supported. """ + + pass diff --git a/optimizely/helpers/audience.py b/optimizely/helpers/audience.py index cd214745..0e822436 100644 --- a/optimizely/helpers/audience.py +++ b/optimizely/helpers/audience.py @@ -19,7 +19,7 @@ def is_user_in_experiment(config, experiment, attributes, logger): - """ Determine for given experiment if user satisfies the audiences for the experiment. + """ Determine for given experiment if user satisfies the audiences for the experiment. Args: config: project_config.ProjectConfig object representing the project. @@ -32,60 +32,48 @@ def is_user_in_experiment(config, experiment, attributes, logger): Boolean representing if user satisfies audience conditions for any of the audiences or not. """ - audience_conditions = experiment.getAudienceConditionsOrIds() + audience_conditions = experiment.getAudienceConditionsOrIds() - logger.debug(audience_logs.EVALUATING_AUDIENCES_COMBINED.format( - experiment.key, - json.dumps(audience_conditions) - )) + logger.debug(audience_logs.EVALUATING_AUDIENCES_COMBINED.format(experiment.key, json.dumps(audience_conditions))) - # Return True in case there are no audiences - if audience_conditions is None or audience_conditions == []: - logger.info(audience_logs.AUDIENCE_EVALUATION_RESULT_COMBINED.format( - experiment.key, - 'TRUE' - )) + # Return True in case there are no audiences + if audience_conditions is None or audience_conditions == []: + logger.info(audience_logs.AUDIENCE_EVALUATION_RESULT_COMBINED.format(experiment.key, 'TRUE')) - return True + return True - if attributes is None: - attributes = {} + if attributes is None: + attributes = {} - def evaluate_custom_attr(audienceId, index): - audience = config.get_audience(audienceId) - custom_attr_condition_evaluator = condition_helper.CustomAttributeConditionEvaluator( - audience.conditionList, attributes, logger) + def evaluate_custom_attr(audienceId, index): + audience = config.get_audience(audienceId) + custom_attr_condition_evaluator = condition_helper.CustomAttributeConditionEvaluator( + audience.conditionList, attributes, logger + ) - return custom_attr_condition_evaluator.evaluate(index) + return custom_attr_condition_evaluator.evaluate(index) - def evaluate_audience(audienceId): - audience = config.get_audience(audienceId) + def evaluate_audience(audienceId): + audience = config.get_audience(audienceId) - if audience is None: - return None + if audience is None: + return None - logger.debug(audience_logs.EVALUATING_AUDIENCE.format(audienceId, audience.conditions)) + logger.debug(audience_logs.EVALUATING_AUDIENCE.format(audienceId, audience.conditions)) - result = condition_tree_evaluator.evaluate( - audience.conditionStructure, - lambda index: evaluate_custom_attr(audienceId, index) - ) + result = condition_tree_evaluator.evaluate( + audience.conditionStructure, lambda index: evaluate_custom_attr(audienceId, index), + ) - result_str = str(result).upper() if result is not None else 'UNKNOWN' - logger.info(audience_logs.AUDIENCE_EVALUATION_RESULT.format(audienceId, result_str)) + result_str = str(result).upper() if result is not None else 'UNKNOWN' + logger.info(audience_logs.AUDIENCE_EVALUATION_RESULT.format(audienceId, result_str)) - return result + return result - eval_result = condition_tree_evaluator.evaluate( - audience_conditions, - evaluate_audience - ) + eval_result = condition_tree_evaluator.evaluate(audience_conditions, evaluate_audience) - eval_result = eval_result or False + eval_result = eval_result or False - logger.info(audience_logs.AUDIENCE_EVALUATION_RESULT_COMBINED.format( - experiment.key, - str(eval_result).upper() - )) + logger.info(audience_logs.AUDIENCE_EVALUATION_RESULT_COMBINED.format(experiment.key, str(eval_result).upper())) - return eval_result + return eval_result diff --git a/optimizely/helpers/condition.py b/optimizely/helpers/condition.py index 48b9227c..0abafb01 100644 --- a/optimizely/helpers/condition.py +++ b/optimizely/helpers/condition.py @@ -21,31 +21,31 @@ class ConditionOperatorTypes(object): - AND = 'and' - OR = 'or' - NOT = 'not' + AND = 'and' + OR = 'or' + NOT = 'not' class ConditionMatchTypes(object): - EXACT = 'exact' - EXISTS = 'exists' - GREATER_THAN = 'gt' - LESS_THAN = 'lt' - SUBSTRING = 'substring' + EXACT = 'exact' + EXISTS = 'exists' + GREATER_THAN = 'gt' + LESS_THAN = 'lt' + SUBSTRING = 'substring' class CustomAttributeConditionEvaluator(object): - """ Class encapsulating methods to be used in audience leaf condition evaluation. """ + """ Class encapsulating methods to be used in audience leaf condition evaluation. """ - CUSTOM_ATTRIBUTE_CONDITION_TYPE = 'custom_attribute' + CUSTOM_ATTRIBUTE_CONDITION_TYPE = 'custom_attribute' - def __init__(self, condition_data, attributes, logger): - self.condition_data = condition_data - self.attributes = attributes or {} - self.logger = logger + def __init__(self, condition_data, attributes, logger): + self.condition_data = condition_data + self.attributes = attributes or {} + self.logger = logger - def _get_condition_json(self, index): - """ Method to generate json for logging audience condition. + def _get_condition_json(self, index): + """ Method to generate json for logging audience condition. Args: index: Index of the condition. @@ -53,18 +53,18 @@ def _get_condition_json(self, index): Returns: String: Audience condition JSON. """ - condition = self.condition_data[index] - condition_log = { - 'name': condition[0], - 'value': condition[1], - 'type': condition[2], - 'match': condition[3] - } + condition = self.condition_data[index] + condition_log = { + 'name': condition[0], + 'value': condition[1], + 'type': condition[2], + 'match': condition[3], + } - return json.dumps(condition_log) + return json.dumps(condition_log) - def is_value_type_valid_for_exact_conditions(self, value): - """ Method to validate if the value is valid for exact match type evaluation. + def is_value_type_valid_for_exact_conditions(self, value): + """ Method to validate if the value is valid for exact match type evaluation. Args: value: Value to validate. @@ -72,20 +72,20 @@ def is_value_type_valid_for_exact_conditions(self, value): Returns: Boolean: True if value is a string, boolean, or number. Otherwise False. """ - # No need to check for bool since bool is a subclass of int - if isinstance(value, string_types) or isinstance(value, (numbers.Integral, float)): - return True + # No need to check for bool since bool is a subclass of int + if isinstance(value, string_types) or isinstance(value, (numbers.Integral, float)): + return True - return False + return False - def is_value_a_number(self, value): - if isinstance(value, (numbers.Integral, float)) and not isinstance(value, bool): - return True + def is_value_a_number(self, value): + if isinstance(value, (numbers.Integral, float)) and not isinstance(value, bool): + return True - return False + return False - def exact_evaluator(self, index): - """ Evaluate the given exact match condition for the user attributes. + def exact_evaluator(self, index): + """ Evaluate the given exact match condition for the user attributes. Args: index: Index of the condition to be evaluated. @@ -98,38 +98,34 @@ def exact_evaluator(self, index): - if the condition value or user attribute value has an invalid type. - if there is a mismatch between the user attribute type and the condition value type. """ - condition_name = self.condition_data[index][0] - condition_value = self.condition_data[index][1] - user_value = self.attributes.get(condition_name) - - if not self.is_value_type_valid_for_exact_conditions(condition_value) or \ - (self.is_value_a_number(condition_value) and not validator.is_finite_number(condition_value)): - self.logger.warning(audience_logs.UNKNOWN_CONDITION_VALUE.format( - self._get_condition_json(index) - )) - return None - - if not self.is_value_type_valid_for_exact_conditions(user_value) or \ - not validator.are_values_same_type(condition_value, user_value): - self.logger.warning(audience_logs.UNEXPECTED_TYPE.format( - self._get_condition_json(index), - type(user_value), - condition_name - )) - return None - - if self.is_value_a_number(user_value) and \ - not validator.is_finite_number(user_value): - self.logger.warning(audience_logs.INFINITE_ATTRIBUTE_VALUE.format( - self._get_condition_json(index), - condition_name - )) - return None - - return condition_value == user_value - - def exists_evaluator(self, index): - """ Evaluate the given exists match condition for the user attributes. + condition_name = self.condition_data[index][0] + condition_value = self.condition_data[index][1] + user_value = self.attributes.get(condition_name) + + if not self.is_value_type_valid_for_exact_conditions(condition_value) or ( + self.is_value_a_number(condition_value) and not validator.is_finite_number(condition_value) + ): + self.logger.warning(audience_logs.UNKNOWN_CONDITION_VALUE.format(self._get_condition_json(index))) + return None + + if not self.is_value_type_valid_for_exact_conditions(user_value) or not validator.are_values_same_type( + condition_value, user_value + ): + self.logger.warning( + audience_logs.UNEXPECTED_TYPE.format(self._get_condition_json(index), type(user_value), condition_name) + ) + return None + + if self.is_value_a_number(user_value) and not validator.is_finite_number(user_value): + self.logger.warning( + audience_logs.INFINITE_ATTRIBUTE_VALUE.format(self._get_condition_json(index), condition_name) + ) + return None + + return condition_value == user_value + + def exists_evaluator(self, index): + """ Evaluate the given exists match condition for the user attributes. Args: index: Index of the condition to be evaluated. @@ -138,11 +134,11 @@ def exists_evaluator(self, index): Boolean: True if the user attributes have a non-null value for the given condition, otherwise False. """ - attr_name = self.condition_data[index][0] - return self.attributes.get(attr_name) is not None + attr_name = self.condition_data[index][0] + return self.attributes.get(attr_name) is not None - def greater_than_evaluator(self, index): - """ Evaluate the given greater than match condition for the user attributes. + def greater_than_evaluator(self, index): + """ Evaluate the given greater than match condition for the user attributes. Args: index: Index of the condition to be evaluated. @@ -153,35 +149,30 @@ def greater_than_evaluator(self, index): - False if the user attribute value is less than or equal to the condition value. None: if the condition value isn't finite or the user attribute value isn't finite. """ - condition_name = self.condition_data[index][0] - condition_value = self.condition_data[index][1] - user_value = self.attributes.get(condition_name) - - if not validator.is_finite_number(condition_value): - self.logger.warning(audience_logs.UNKNOWN_CONDITION_VALUE.format( - self._get_condition_json(index) - )) - return None - - if not self.is_value_a_number(user_value): - self.logger.warning(audience_logs.UNEXPECTED_TYPE.format( - self._get_condition_json(index), - type(user_value), - condition_name - )) - return None - - if not validator.is_finite_number(user_value): - self.logger.warning(audience_logs.INFINITE_ATTRIBUTE_VALUE.format( - self._get_condition_json(index), - condition_name - )) - return None - - return user_value > condition_value - - def less_than_evaluator(self, index): - """ Evaluate the given less than match condition for the user attributes. + condition_name = self.condition_data[index][0] + condition_value = self.condition_data[index][1] + user_value = self.attributes.get(condition_name) + + if not validator.is_finite_number(condition_value): + self.logger.warning(audience_logs.UNKNOWN_CONDITION_VALUE.format(self._get_condition_json(index))) + return None + + if not self.is_value_a_number(user_value): + self.logger.warning( + audience_logs.UNEXPECTED_TYPE.format(self._get_condition_json(index), type(user_value), condition_name) + ) + return None + + if not validator.is_finite_number(user_value): + self.logger.warning( + audience_logs.INFINITE_ATTRIBUTE_VALUE.format(self._get_condition_json(index), condition_name) + ) + return None + + return user_value > condition_value + + def less_than_evaluator(self, index): + """ Evaluate the given less than match condition for the user attributes. Args: index: Index of the condition to be evaluated. @@ -192,35 +183,30 @@ def less_than_evaluator(self, index): - False if the user attribute value is greater than or equal to the condition value. None: if the condition value isn't finite or the user attribute value isn't finite. """ - condition_name = self.condition_data[index][0] - condition_value = self.condition_data[index][1] - user_value = self.attributes.get(condition_name) - - if not validator.is_finite_number(condition_value): - self.logger.warning(audience_logs.UNKNOWN_CONDITION_VALUE.format( - self._get_condition_json(index) - )) - return None - - if not self.is_value_a_number(user_value): - self.logger.warning(audience_logs.UNEXPECTED_TYPE.format( - self._get_condition_json(index), - type(user_value), - condition_name - )) - return None - - if not validator.is_finite_number(user_value): - self.logger.warning(audience_logs.INFINITE_ATTRIBUTE_VALUE.format( - self._get_condition_json(index), - condition_name - )) - return None - - return user_value < condition_value - - def substring_evaluator(self, index): - """ Evaluate the given substring match condition for the given user attributes. + condition_name = self.condition_data[index][0] + condition_value = self.condition_data[index][1] + user_value = self.attributes.get(condition_name) + + if not validator.is_finite_number(condition_value): + self.logger.warning(audience_logs.UNKNOWN_CONDITION_VALUE.format(self._get_condition_json(index))) + return None + + if not self.is_value_a_number(user_value): + self.logger.warning( + audience_logs.UNEXPECTED_TYPE.format(self._get_condition_json(index), type(user_value), condition_name) + ) + return None + + if not validator.is_finite_number(user_value): + self.logger.warning( + audience_logs.INFINITE_ATTRIBUTE_VALUE.format(self._get_condition_json(index), condition_name) + ) + return None + + return user_value < condition_value + + def substring_evaluator(self, index): + """ Evaluate the given substring match condition for the given user attributes. Args: index: Index of the condition to be evaluated. @@ -231,36 +217,32 @@ def substring_evaluator(self, index): - False if the condition value is not a substring of the user attribute value. None: if the condition value isn't a string or the user attribute value isn't a string. """ - condition_name = self.condition_data[index][0] - condition_value = self.condition_data[index][1] - user_value = self.attributes.get(condition_name) - - if not isinstance(condition_value, string_types): - self.logger.warning(audience_logs.UNKNOWN_CONDITION_VALUE.format( - self._get_condition_json(index), - )) - return None - - if not isinstance(user_value, string_types): - self.logger.warning(audience_logs.UNEXPECTED_TYPE.format( - self._get_condition_json(index), - type(user_value), - condition_name - )) - return None - - return condition_value in user_value - - EVALUATORS_BY_MATCH_TYPE = { - ConditionMatchTypes.EXACT: exact_evaluator, - ConditionMatchTypes.EXISTS: exists_evaluator, - ConditionMatchTypes.GREATER_THAN: greater_than_evaluator, - ConditionMatchTypes.LESS_THAN: less_than_evaluator, - ConditionMatchTypes.SUBSTRING: substring_evaluator - } - - def evaluate(self, index): - """ Given a custom attribute audience condition and user attributes, evaluate the + condition_name = self.condition_data[index][0] + condition_value = self.condition_data[index][1] + user_value = self.attributes.get(condition_name) + + if not isinstance(condition_value, string_types): + self.logger.warning(audience_logs.UNKNOWN_CONDITION_VALUE.format(self._get_condition_json(index),)) + return None + + if not isinstance(user_value, string_types): + self.logger.warning( + audience_logs.UNEXPECTED_TYPE.format(self._get_condition_json(index), type(user_value), condition_name) + ) + return None + + return condition_value in user_value + + EVALUATORS_BY_MATCH_TYPE = { + ConditionMatchTypes.EXACT: exact_evaluator, + ConditionMatchTypes.EXISTS: exists_evaluator, + ConditionMatchTypes.GREATER_THAN: greater_than_evaluator, + ConditionMatchTypes.LESS_THAN: less_than_evaluator, + ConditionMatchTypes.SUBSTRING: substring_evaluator, + } + + def evaluate(self, index): + """ Given a custom attribute audience condition and user attributes, evaluate the condition against the attributes. Args: @@ -273,42 +255,46 @@ def evaluate(self, index): None: if the user attributes and condition can't be evaluated. """ - if self.condition_data[index][2] != self.CUSTOM_ATTRIBUTE_CONDITION_TYPE: - self.logger.warning(audience_logs.UNKNOWN_CONDITION_TYPE.format(self._get_condition_json(index))) - return None + if self.condition_data[index][2] != self.CUSTOM_ATTRIBUTE_CONDITION_TYPE: + self.logger.warning(audience_logs.UNKNOWN_CONDITION_TYPE.format(self._get_condition_json(index))) + return None - condition_match = self.condition_data[index][3] - if condition_match is None: - condition_match = ConditionMatchTypes.EXACT + condition_match = self.condition_data[index][3] + if condition_match is None: + condition_match = ConditionMatchTypes.EXACT - if condition_match not in self.EVALUATORS_BY_MATCH_TYPE: - self.logger.warning(audience_logs.UNKNOWN_MATCH_TYPE.format(self._get_condition_json(index))) - return None + if condition_match not in self.EVALUATORS_BY_MATCH_TYPE: + self.logger.warning(audience_logs.UNKNOWN_MATCH_TYPE.format(self._get_condition_json(index))) + return None - if condition_match != ConditionMatchTypes.EXISTS: - attribute_key = self.condition_data[index][0] - if attribute_key not in self.attributes: - self.logger.debug(audience_logs.MISSING_ATTRIBUTE_VALUE.format(self._get_condition_json(index), attribute_key)) - return None + if condition_match != ConditionMatchTypes.EXISTS: + attribute_key = self.condition_data[index][0] + if attribute_key not in self.attributes: + self.logger.debug( + audience_logs.MISSING_ATTRIBUTE_VALUE.format(self._get_condition_json(index), attribute_key) + ) + return None - if self.attributes.get(attribute_key) is None: - self.logger.debug(audience_logs.NULL_ATTRIBUTE_VALUE.format(self._get_condition_json(index), attribute_key)) - return None + if self.attributes.get(attribute_key) is None: + self.logger.debug( + audience_logs.NULL_ATTRIBUTE_VALUE.format(self._get_condition_json(index), attribute_key) + ) + return None - return self.EVALUATORS_BY_MATCH_TYPE[condition_match](self, index) + return self.EVALUATORS_BY_MATCH_TYPE[condition_match](self, index) class ConditionDecoder(object): - """ Class which provides an object_hook method for decoding dict + """ Class which provides an object_hook method for decoding dict objects into a list when given a condition_decoder. """ - def __init__(self, condition_decoder): - self.condition_list = [] - self.index = -1 - self.decoder = condition_decoder + def __init__(self, condition_decoder): + self.condition_list = [] + self.index = -1 + self.decoder = condition_decoder - def object_hook(self, object_dict): - """ Hook which when passed into a json.JSONDecoder will replace each dict + def object_hook(self, object_dict): + """ Hook which when passed into a json.JSONDecoder will replace each dict in a json string with its index and convert the dict to an object as defined by the passed in condition_decoder. The newly created condition object is appended to the conditions_list. @@ -319,14 +305,14 @@ def object_hook(self, object_dict): Returns: An index which will be used as the placeholder in the condition_structure """ - instance = self.decoder(object_dict) - self.condition_list.append(instance) - self.index += 1 - return self.index + instance = self.decoder(object_dict) + self.condition_list.append(instance) + self.index += 1 + return self.index def _audience_condition_deserializer(obj_dict): - """ Deserializer defining how dict objects need to be decoded for audience conditions. + """ Deserializer defining how dict objects need to be decoded for audience conditions. Args: obj_dict: Dict representing one audience condition. @@ -334,16 +320,16 @@ def _audience_condition_deserializer(obj_dict): Returns: List consisting of condition key with corresponding value, type and match. """ - return [ - obj_dict.get('name'), - obj_dict.get('value'), - obj_dict.get('type'), - obj_dict.get('match') - ] + return [ + obj_dict.get('name'), + obj_dict.get('value'), + obj_dict.get('type'), + obj_dict.get('match'), + ] def loads(conditions_string): - """ Deserializes the conditions property into its corresponding + """ Deserializes the conditions property into its corresponding components: the condition_structure and the condition_list. Args: @@ -354,14 +340,14 @@ def loads(conditions_string): condition_structure: nested list of operators and placeholders for operands. condition_list: list of conditions whose index correspond to the values of the placeholders. """ - decoder = ConditionDecoder(_audience_condition_deserializer) + decoder = ConditionDecoder(_audience_condition_deserializer) - # Create a custom JSONDecoder using the ConditionDecoder's object_hook method - # to create the condition_structure as well as populate the condition_list - json_decoder = json.JSONDecoder(object_hook=decoder.object_hook) + # Create a custom JSONDecoder using the ConditionDecoder's object_hook method + # to create the condition_structure as well as populate the condition_list + json_decoder = json.JSONDecoder(object_hook=decoder.object_hook) - # Perform the decoding - condition_structure = json_decoder.decode(conditions_string) - condition_list = decoder.condition_list + # Perform the decoding + condition_structure = json_decoder.decode(conditions_string) + condition_list = decoder.condition_list - return (condition_structure, condition_list) + return (condition_structure, condition_list) diff --git a/optimizely/helpers/condition_tree_evaluator.py b/optimizely/helpers/condition_tree_evaluator.py index ae88c414..c0fe7b87 100644 --- a/optimizely/helpers/condition_tree_evaluator.py +++ b/optimizely/helpers/condition_tree_evaluator.py @@ -15,7 +15,7 @@ def and_evaluator(conditions, leaf_evaluator): - """ Evaluates a list of conditions as if the evaluator had been applied + """ Evaluates a list of conditions as if the evaluator had been applied to each entry and the results AND-ed together. Args: @@ -28,20 +28,20 @@ def and_evaluator(conditions, leaf_evaluator): - False if a single operand evaluates to False. None: if conditions couldn't be evaluated. """ - saw_null_result = False + saw_null_result = False - for condition in conditions: - result = evaluate(condition, leaf_evaluator) - if result is False: - return False - if result is None: - saw_null_result = True + for condition in conditions: + result = evaluate(condition, leaf_evaluator) + if result is False: + return False + if result is None: + saw_null_result = True - return None if saw_null_result else True + return None if saw_null_result else True def or_evaluator(conditions, leaf_evaluator): - """ Evaluates a list of conditions as if the evaluator had been applied + """ Evaluates a list of conditions as if the evaluator had been applied to each entry and the results OR-ed together. Args: @@ -54,20 +54,20 @@ def or_evaluator(conditions, leaf_evaluator): - False if all operands evaluate to False. None: if conditions couldn't be evaluated. """ - saw_null_result = False + saw_null_result = False - for condition in conditions: - result = evaluate(condition, leaf_evaluator) - if result is True: - return True - if result is None: - saw_null_result = True + for condition in conditions: + result = evaluate(condition, leaf_evaluator) + if result is True: + return True + if result is None: + saw_null_result = True - return None if saw_null_result else False + return None if saw_null_result else False def not_evaluator(conditions, leaf_evaluator): - """ Evaluates a list of conditions as if the evaluator had been applied + """ Evaluates a list of conditions as if the evaluator had been applied to a single entry and NOT was applied to the result. Args: @@ -80,22 +80,22 @@ def not_evaluator(conditions, leaf_evaluator): - False if the operand evaluates to True. None: if conditions is empty or condition couldn't be evaluated. """ - if not len(conditions) > 0: - return None + if not len(conditions) > 0: + return None - result = evaluate(conditions[0], leaf_evaluator) - return None if result is None else not result + result = evaluate(conditions[0], leaf_evaluator) + return None if result is None else not result EVALUATORS_BY_OPERATOR_TYPE = { - ConditionOperatorTypes.AND: and_evaluator, - ConditionOperatorTypes.OR: or_evaluator, - ConditionOperatorTypes.NOT: not_evaluator + ConditionOperatorTypes.AND: and_evaluator, + ConditionOperatorTypes.OR: or_evaluator, + ConditionOperatorTypes.NOT: not_evaluator, } def evaluate(conditions, leaf_evaluator): - """ Top level method to evaluate conditions. + """ Top level method to evaluate conditions. Args: conditions: Nested array of and/or conditions, or a single leaf condition value of any type. @@ -108,12 +108,12 @@ def evaluate(conditions, leaf_evaluator): """ - if isinstance(conditions, list): - if conditions[0] in list(EVALUATORS_BY_OPERATOR_TYPE.keys()): - return EVALUATORS_BY_OPERATOR_TYPE[conditions[0]](conditions[1:], leaf_evaluator) - else: - # assume OR when operator is not explicit. - return EVALUATORS_BY_OPERATOR_TYPE[ConditionOperatorTypes.OR](conditions, leaf_evaluator) + if isinstance(conditions, list): + if conditions[0] in list(EVALUATORS_BY_OPERATOR_TYPE.keys()): + return EVALUATORS_BY_OPERATOR_TYPE[conditions[0]](conditions[1:], leaf_evaluator) + else: + # assume OR when operator is not explicit. + return EVALUATORS_BY_OPERATOR_TYPE[ConditionOperatorTypes.OR](conditions, leaf_evaluator) - leaf_condition = conditions - return leaf_evaluator(leaf_condition) + leaf_condition = conditions + return leaf_evaluator(leaf_condition) diff --git a/optimizely/helpers/constants.py b/optimizely/helpers/constants.py index a9cb3b97..06803152 100644 --- a/optimizely/helpers/constants.py +++ b/optimizely/helpers/constants.py @@ -12,284 +12,153 @@ # limitations under the License. JSON_SCHEMA = { - "$schema": "http://json-schema.org/draft-04/schema#", - "type": "object", - "properties": { - "projectId": { - "type": "string" - }, - "accountId": { - "type": "string" - }, - "groups": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "policy": { - "type": "string" - }, - "trafficAllocation": { - "type": "array", - "items": { - "type": "object", - "properties": { - "entityId": { - "type": "string" - }, - "endOfRange": { - "type": "integer" - } - }, - "required": [ - "entityId", - "endOfRange" - ] - } - }, - "experiments": { + "$schema": "http://json-schema.org/draft-04/schema#", + "type": "object", + "properties": { + "projectId": {"type": "string"}, + "accountId": {"type": "string"}, + "groups": { "type": "array", "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "layerId": { - "type": "string" - }, - "key": { - "type": "string" - }, - "status": { - "type": "string" - }, - "variations": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "key": { - "type": "string" - } + "type": "object", + "properties": { + "id": {"type": "string"}, + "policy": {"type": "string"}, + "trafficAllocation": { + "type": "array", + "items": { + "type": "object", + "properties": {"entityId": {"type": "string"}, "endOfRange": {"type": "integer"}}, + "required": ["entityId", "endOfRange"], + }, }, - "required": [ - "id", - "key" - ] - } - }, - "trafficAllocation": { - "type": "array", - "items": { - "type": "object", - "properties": { - "entityId": { - "type": "string" - }, - "endOfRange": { - "type": "integer" - } + "experiments": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": {"type": "string"}, + "layerId": {"type": "string"}, + "key": {"type": "string"}, + "status": {"type": "string"}, + "variations": { + "type": "array", + "items": { + "type": "object", + "properties": {"id": {"type": "string"}, "key": {"type": "string"}}, + "required": ["id", "key"], + }, + }, + "trafficAllocation": { + "type": "array", + "items": { + "type": "object", + "properties": { + "entityId": {"type": "string"}, + "endOfRange": {"type": "integer"}, + }, + "required": ["entityId", "endOfRange"], + }, + }, + "audienceIds": {"type": "array", "items": {"type": "string"}}, + "forcedVariations": {"type": "object"}, + }, + "required": [ + "id", + "layerId", + "key", + "status", + "variations", + "trafficAllocation", + "audienceIds", + "forcedVariations", + ], + }, }, - "required": [ - "entityId", - "endOfRange" - ] - } - }, - "audienceIds": { - "type": "array", - "items": { - "type": "string" - } }, - "forcedVariations": { - "type": "object" - } - }, - "required": [ - "id", - "layerId", - "key", - "status", - "variations", - "trafficAllocation", - "audienceIds", - "forcedVariations" - ] - } - } + "required": ["id", "policy", "trafficAllocation", "experiments"], + }, }, - "required": [ - "id", - "policy", - "trafficAllocation", - "experiments" - ] - }, - }, - "experiments": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "layerId": { - "type": "string" - }, - "key": { - "type": "string" - }, - "status": { - "type": "string" - }, - "variations": { + "experiments": { "type": "array", "items": { - "type": "object", - "properties": { - "id": { - "type": "string" + "type": "object", + "properties": { + "id": {"type": "string"}, + "layerId": {"type": "string"}, + "key": {"type": "string"}, + "status": {"type": "string"}, + "variations": { + "type": "array", + "items": { + "type": "object", + "properties": {"id": {"type": "string"}, "key": {"type": "string"}}, + "required": ["id", "key"], + }, + }, + "trafficAllocation": { + "type": "array", + "items": { + "type": "object", + "properties": {"entityId": {"type": "string"}, "endOfRange": {"type": "integer"}}, + "required": ["entityId", "endOfRange"], + }, + }, + "audienceIds": {"type": "array", "items": {"type": "string"}}, + "forcedVariations": {"type": "object"}, }, - "key": { - "type": "string" - } - }, - "required": [ - "id", - "key" - ] - } - }, - "trafficAllocation": { + "required": [ + "id", + "layerId", + "key", + "status", + "variations", + "trafficAllocation", + "audienceIds", + "forcedVariations", + ], + }, + }, + "events": { "type": "array", "items": { - "type": "object", - "properties": { - "entityId": { - "type": "string" + "type": "object", + "properties": { + "key": {"type": "string"}, + "experimentIds": {"type": "array", "items": {"type": "string"}}, + "id": {"type": "string"}, }, - "endOfRange": { - "type": "integer" - } - }, - "required": [ - "entityId", - "endOfRange" - ] - } - }, - "audienceIds": { + "required": ["key", "experimentIds", "id"], + }, + }, + "audiences": { "type": "array", "items": { - "type": "string" - } - }, - "forcedVariations": { - "type": "object" - } + "type": "object", + "properties": {"id": {"type": "string"}, "name": {"type": "string"}, "conditions": {"type": "string"}}, + "required": ["id", "name", "conditions"], + }, }, - "required": [ - "id", - "layerId", - "key", - "status", - "variations", - "trafficAllocation", - "audienceIds", - "forcedVariations" - ] - } - }, - "events": { - "type": "array", - "items": { - "type": "object", - "properties": { - "key": { - "type": "string" - }, - "experimentIds": { + "attributes": { "type": "array", "items": { - "type": "string" - } - }, - "id": { - "type": "string" - } + "type": "object", + "properties": {"id": {"type": "string"}, "key": {"type": "string"}}, + "required": ["id", "key"], + }, }, - "required": [ - "key", - "experimentIds", - "id" - ] - } - }, - "audiences": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "name": { - "type": "string" - }, - "conditions": { - "type": "string" - } - }, - "required": [ - "id", - "name", - "conditions" - ] - } - }, - "attributes": { - "type": "array", - "items": { - "type": "object", - "properties": { - "id": { - "type": "string" - }, - "key": { - "type": "string" - } - }, - "required": [ - "id", - "key", - ] - } - }, - "version": { - "type": "string" - }, - "revision": { - "type": "string" + "version": {"type": "string"}, + "revision": {"type": "string"}, }, - }, - "required": [ - "projectId", - "accountId", - "groups", - "experiments", - "events", - "audiences", - "attributes", - "version", - "revision", - ] + "required": [ + "projectId", + "accountId", + "groups", + "experiments", + "events", + "audiences", + "attributes", + "version", + "revision", + ], } diff --git a/optimizely/helpers/enums.py b/optimizely/helpers/enums.py index d0cc06c3..3a911417 100644 --- a/optimizely/helpers/enums.py +++ b/optimizely/helpers/enums.py @@ -15,102 +15,114 @@ class AudienceEvaluationLogs(object): - AUDIENCE_EVALUATION_RESULT = 'Audience "{}" evaluated to {}.' - AUDIENCE_EVALUATION_RESULT_COMBINED = 'Audiences for experiment "{}" collectively evaluated to {}.' - EVALUATING_AUDIENCE = 'Starting to evaluate audience "{}" with conditions: {}.' - EVALUATING_AUDIENCES_COMBINED = 'Evaluating audiences for experiment "{}": {}.' - INFINITE_ATTRIBUTE_VALUE = 'Audience condition "{}" evaluated to UNKNOWN because the number value ' \ - 'for user attribute "{}" is not in the range [-2^53, +2^53].' - MISSING_ATTRIBUTE_VALUE = 'Audience condition {} evaluated to UNKNOWN because no value was passed for '\ - 'user attribute "{}".' - NULL_ATTRIBUTE_VALUE = 'Audience condition "{}" evaluated to UNKNOWN because a null value was passed '\ - 'for user attribute "{}".' - UNEXPECTED_TYPE = 'Audience condition "{}" evaluated to UNKNOWN because a value of type "{}" was passed '\ - 'for user attribute "{}".' - - UNKNOWN_CONDITION_TYPE = 'Audience condition "{}" uses an unknown condition type. You may need to upgrade to a '\ - 'newer release of the Optimizely SDK.' - UNKNOWN_CONDITION_VALUE = 'Audience condition "{}" has an unsupported condition value. You may need to upgrade to a '\ - 'newer release of the Optimizely SDK.' - UNKNOWN_MATCH_TYPE = 'Audience condition "{}" uses an unknown match type. You may need to upgrade to a '\ - 'newer release of the Optimizely SDK.' + AUDIENCE_EVALUATION_RESULT = 'Audience "{}" evaluated to {}.' + AUDIENCE_EVALUATION_RESULT_COMBINED = 'Audiences for experiment "{}" collectively evaluated to {}.' + EVALUATING_AUDIENCE = 'Starting to evaluate audience "{}" with conditions: {}.' + EVALUATING_AUDIENCES_COMBINED = 'Evaluating audiences for experiment "{}": {}.' + INFINITE_ATTRIBUTE_VALUE = ( + 'Audience condition "{}" evaluated to UNKNOWN because the number value ' + 'for user attribute "{}" is not in the range [-2^53, +2^53].' + ) + MISSING_ATTRIBUTE_VALUE = ( + 'Audience condition {} evaluated to UNKNOWN because no value was passed for ' 'user attribute "{}".' + ) + NULL_ATTRIBUTE_VALUE = ( + 'Audience condition "{}" evaluated to UNKNOWN because a null value was passed ' 'for user attribute "{}".' + ) + UNEXPECTED_TYPE = ( + 'Audience condition "{}" evaluated to UNKNOWN because a value of type "{}" was passed ' + 'for user attribute "{}".' + ) + + UNKNOWN_CONDITION_TYPE = ( + 'Audience condition "{}" uses an unknown condition type. You may need to upgrade to a ' + 'newer release of the Optimizely SDK.' + ) + UNKNOWN_CONDITION_VALUE = ( + 'Audience condition "{}" has an unsupported condition value. You may need to upgrade to a ' + 'newer release of the Optimizely SDK.' + ) + UNKNOWN_MATCH_TYPE = ( + 'Audience condition "{}" uses an unknown match type. You may need to upgrade to a ' + 'newer release of the Optimizely SDK.' + ) class ConfigManager(object): - DATAFILE_URL_TEMPLATE = 'https://cdn.optimizely.com/datafiles/{sdk_key}.json' - # Default time in seconds to block the 'get_config' method call until 'config' instance has been initialized. - DEFAULT_BLOCKING_TIMEOUT = 10 - # Default config update interval of 5 minutes - DEFAULT_UPDATE_INTERVAL = 5 * 60 - # Time in seconds before which request for datafile times out - REQUEST_TIMEOUT = 10 + DATAFILE_URL_TEMPLATE = 'https://cdn.optimizely.com/datafiles/{sdk_key}.json' + # Default time in seconds to block the 'get_config' method call until 'config' instance has been initialized. + DEFAULT_BLOCKING_TIMEOUT = 10 + # Default config update interval of 5 minutes + DEFAULT_UPDATE_INTERVAL = 5 * 60 + # Time in seconds before which request for datafile times out + REQUEST_TIMEOUT = 10 class ControlAttributes(object): - BOT_FILTERING = '$opt_bot_filtering' - BUCKETING_ID = '$opt_bucketing_id' - USER_AGENT = '$opt_user_agent' + BOT_FILTERING = '$opt_bot_filtering' + BUCKETING_ID = '$opt_bucketing_id' + USER_AGENT = '$opt_user_agent' class DatafileVersions(object): - V2 = '2' - V3 = '3' - V4 = '4' + V2 = '2' + V3 = '3' + V4 = '4' class DecisionNotificationTypes(object): - AB_TEST = 'ab-test' - FEATURE = 'feature' - FEATURE_TEST = 'feature-test' - FEATURE_VARIABLE = 'feature-variable' + AB_TEST = 'ab-test' + FEATURE = 'feature' + FEATURE_TEST = 'feature-test' + FEATURE_VARIABLE = 'feature-variable' class DecisionSources(object): - FEATURE_TEST = 'feature-test' - ROLLOUT = 'rollout' + FEATURE_TEST = 'feature-test' + ROLLOUT = 'rollout' class Errors(object): - INVALID_ATTRIBUTE = 'Provided attribute is not in datafile.' - INVALID_ATTRIBUTE_FORMAT = 'Attributes provided are in an invalid format.' - INVALID_AUDIENCE = 'Provided audience is not in datafile.' - INVALID_EVENT_TAG_FORMAT = 'Event tags provided are in an invalid format.' - INVALID_EXPERIMENT_KEY = 'Provided experiment is not in datafile.' - INVALID_EVENT_KEY = 'Provided event is not in datafile.' - INVALID_FEATURE_KEY = 'Provided feature key is not in the datafile.' - INVALID_GROUP_ID = 'Provided group is not in datafile.' - INVALID_INPUT = 'Provided "{}" is in an invalid format.' - INVALID_OPTIMIZELY = 'Optimizely instance is not valid. Failing "{}".' - INVALID_PROJECT_CONFIG = 'Invalid config. Optimizely instance is not valid. Failing "{}".' - INVALID_VARIATION = 'Provided variation is not in datafile.' - INVALID_VARIABLE_KEY = 'Provided variable key is not in the feature flag.' - NONE_FEATURE_KEY_PARAMETER = '"None" is an invalid value for feature key.' - NONE_USER_ID_PARAMETER = '"None" is an invalid value for user ID.' - NONE_VARIABLE_KEY_PARAMETER = '"None" is an invalid value for variable key.' - UNSUPPORTED_DATAFILE_VERSION = 'This version of the Python SDK does not support the given datafile version: "{}".' + INVALID_ATTRIBUTE = 'Provided attribute is not in datafile.' + INVALID_ATTRIBUTE_FORMAT = 'Attributes provided are in an invalid format.' + INVALID_AUDIENCE = 'Provided audience is not in datafile.' + INVALID_EVENT_TAG_FORMAT = 'Event tags provided are in an invalid format.' + INVALID_EXPERIMENT_KEY = 'Provided experiment is not in datafile.' + INVALID_EVENT_KEY = 'Provided event is not in datafile.' + INVALID_FEATURE_KEY = 'Provided feature key is not in the datafile.' + INVALID_GROUP_ID = 'Provided group is not in datafile.' + INVALID_INPUT = 'Provided "{}" is in an invalid format.' + INVALID_OPTIMIZELY = 'Optimizely instance is not valid. Failing "{}".' + INVALID_PROJECT_CONFIG = 'Invalid config. Optimizely instance is not valid. Failing "{}".' + INVALID_VARIATION = 'Provided variation is not in datafile.' + INVALID_VARIABLE_KEY = 'Provided variable key is not in the feature flag.' + NONE_FEATURE_KEY_PARAMETER = '"None" is an invalid value for feature key.' + NONE_USER_ID_PARAMETER = '"None" is an invalid value for user ID.' + NONE_VARIABLE_KEY_PARAMETER = '"None" is an invalid value for variable key.' + UNSUPPORTED_DATAFILE_VERSION = 'This version of the Python SDK does not support the given datafile version: "{}".' class HTTPHeaders(object): - IF_MODIFIED_SINCE = 'If-Modified-Since' - LAST_MODIFIED = 'Last-Modified' + IF_MODIFIED_SINCE = 'If-Modified-Since' + LAST_MODIFIED = 'Last-Modified' class HTTPVerbs(object): - GET = 'GET' - POST = 'POST' + GET = 'GET' + POST = 'POST' class LogLevels(object): - NOTSET = logging.NOTSET - DEBUG = logging.DEBUG - INFO = logging.INFO - WARNING = logging.WARNING - ERROR = logging.ERROR - CRITICAL = logging.CRITICAL + NOTSET = logging.NOTSET + DEBUG = logging.DEBUG + INFO = logging.INFO + WARNING = logging.WARNING + ERROR = logging.ERROR + CRITICAL = logging.CRITICAL class NotificationTypes(object): - """ NotificationTypes for the notification_center.NotificationCenter + """ NotificationTypes for the notification_center.NotificationCenter format is NOTIFICATION TYPE: list of parameters to callback. ACTIVATE (DEPRECATED since 3.1.0) notification listener has the following parameters: @@ -127,8 +139,9 @@ class NotificationTypes(object): LOG_EVENT notification listener has the following parameter(s): LogEvent log_event """ - ACTIVATE = 'ACTIVATE:experiment, user_id, attributes, variation, event' - DECISION = 'DECISION:type, user_id, attributes, decision_info' - OPTIMIZELY_CONFIG_UPDATE = 'OPTIMIZELY_CONFIG_UPDATE' - TRACK = 'TRACK:event_key, user_id, attributes, event_tags, event' - LOG_EVENT = 'LOG_EVENT:log_event' + + ACTIVATE = 'ACTIVATE:experiment, user_id, attributes, variation, event' + DECISION = 'DECISION:type, user_id, attributes, decision_info' + OPTIMIZELY_CONFIG_UPDATE = 'OPTIMIZELY_CONFIG_UPDATE' + TRACK = 'TRACK:event_key, user_id, attributes, event_tags, event' + LOG_EVENT = 'LOG_EVENT:log_event' diff --git a/optimizely/helpers/event_tag_utils.py b/optimizely/helpers/event_tag_utils.py index 06bd953c..0a5ae264 100644 --- a/optimizely/helpers/event_tag_utils.py +++ b/optimizely/helpers/event_tag_utils.py @@ -20,28 +20,28 @@ def get_revenue_value(event_tags): - if event_tags is None: - return None + if event_tags is None: + return None - if not isinstance(event_tags, dict): - return None + if not isinstance(event_tags, dict): + return None - if REVENUE_METRIC_TYPE not in event_tags: - return None + if REVENUE_METRIC_TYPE not in event_tags: + return None - raw_value = event_tags[REVENUE_METRIC_TYPE] + raw_value = event_tags[REVENUE_METRIC_TYPE] - if isinstance(raw_value, bool): - return None + if isinstance(raw_value, bool): + return None - if not isinstance(raw_value, numbers.Integral): - return None + if not isinstance(raw_value, numbers.Integral): + return None - return raw_value + return raw_value def get_numeric_value(event_tags, logger=None): - """ + """ A smart getter of the numeric value from the event tags. Args: @@ -63,63 +63,68 @@ def get_numeric_value(event_tags, logger=None): - Any values that cannot be cast to a float (e.g., an array or dictionary) """ - logger_message_debug = None - numeric_metric_value = None + logger_message_debug = None + numeric_metric_value = None + + if event_tags is None: + return numeric_metric_value + elif not isinstance(event_tags, dict): + if logger: + logger.log(enums.LogLevels.ERROR, 'Event tags is not a dictionary.') + return numeric_metric_value + elif NUMERIC_METRIC_TYPE not in event_tags: + return numeric_metric_value + else: + numeric_metric_value = event_tags[NUMERIC_METRIC_TYPE] + try: + if isinstance(numeric_metric_value, (numbers.Integral, float, str)): + # Attempt to convert the numeric metric value to a float + # (if it isn't already a float). + cast_numeric_metric_value = float(numeric_metric_value) + + # If not a float after casting, then make everything else a None. + # Other potential values are nan, inf, and -inf. + if not isinstance(cast_numeric_metric_value, float) or \ + math.isnan(cast_numeric_metric_value) or \ + math.isinf(cast_numeric_metric_value): + logger_message_debug = 'Provided numeric value {} is in an invalid format.'.format( + numeric_metric_value + ) + numeric_metric_value = None + else: + # Handle booleans as a special case. + # They are treated like an integer in the cast, but we do not want to cast this. + if isinstance(numeric_metric_value, bool): + logger_message_debug = 'Provided numeric value is a boolean, which is an invalid format.' + numeric_metric_value = None + else: + numeric_metric_value = cast_numeric_metric_value + else: + logger_message_debug = 'Numeric metric value is not in integer, float, or string form.' + numeric_metric_value = None + + except ValueError: + logger_message_debug = 'Value error while casting numeric metric value to a float.' + numeric_metric_value = None + + # Log all potential debug messages while converting the numeric value to a float. + if logger and logger_message_debug: + logger.log(enums.LogLevels.DEBUG, logger_message_debug) + + # Log the final numeric metric value + if numeric_metric_value is not None: + if logger: + logger.log( + enums.LogLevels.INFO, + 'The numeric metric value {} will be sent to results.'.format(numeric_metric_value), + ) + else: + if logger: + logger.log( + enums.LogLevels.WARNING, + 'The provided numeric metric value {} is in an invalid format and will not be sent to results.'.format( + numeric_metric_value + ), + ) - if event_tags is None: - return numeric_metric_value - elif not isinstance(event_tags, dict): - if logger: - logger.log(enums.LogLevels.ERROR, 'Event tags is not a dictionary.') - return numeric_metric_value - elif NUMERIC_METRIC_TYPE not in event_tags: return numeric_metric_value - else: - numeric_metric_value = event_tags[NUMERIC_METRIC_TYPE] - try: - if isinstance(numeric_metric_value, (numbers.Integral, float, str)): - # Attempt to convert the numeric metric value to a float - # (if it isn't already a float). - cast_numeric_metric_value = float(numeric_metric_value) - - # If not a float after casting, then make everything else a None. - # Other potential values are nan, inf, and -inf. - if not isinstance(cast_numeric_metric_value, float) \ - or math.isnan(cast_numeric_metric_value) \ - or math.isinf(cast_numeric_metric_value): - logger_message_debug = 'Provided numeric value {} is in an invalid format.'\ - .format(numeric_metric_value) - numeric_metric_value = None - else: - # Handle booleans as a special case. - # They are treated like an integer in the cast, but we do not want to cast this. - if isinstance(numeric_metric_value, bool): - logger_message_debug = 'Provided numeric value is a boolean, which is an invalid format.' - numeric_metric_value = None - else: - numeric_metric_value = cast_numeric_metric_value - else: - logger_message_debug = 'Numeric metric value is not in integer, float, or string form.' - numeric_metric_value = None - - except ValueError: - logger_message_debug = 'Value error while casting numeric metric value to a float.' - numeric_metric_value = None - - # Log all potential debug messages while converting the numeric value to a float. - if logger and logger_message_debug: - logger.log(enums.LogLevels.DEBUG, logger_message_debug) - - # Log the final numeric metric value - if numeric_metric_value is not None: - if logger: - logger.log(enums.LogLevels.INFO, - 'The numeric metric value {} will be sent to results.' - .format(numeric_metric_value)) - else: - if logger: - logger.log(enums.LogLevels.WARNING, - 'The provided numeric metric value {} is in an invalid format and will not be sent to results.' - .format(numeric_metric_value)) - - return numeric_metric_value diff --git a/optimizely/helpers/experiment.py b/optimizely/helpers/experiment.py index 6d1c21e0..45bdd1b5 100644 --- a/optimizely/helpers/experiment.py +++ b/optimizely/helpers/experiment.py @@ -15,7 +15,7 @@ def is_experiment_running(experiment): - """ Determine for given experiment if experiment is running. + """ Determine for given experiment if experiment is running. Args: experiment: Object representing the experiment. @@ -24,4 +24,4 @@ def is_experiment_running(experiment): Boolean representing if experiment is running or not. """ - return experiment.status in ALLOWED_EXPERIMENT_STATUS + return experiment.status in ALLOWED_EXPERIMENT_STATUS diff --git a/optimizely/helpers/validator.py b/optimizely/helpers/validator.py index 441d868d..522faccd 100644 --- a/optimizely/helpers/validator.py +++ b/optimizely/helpers/validator.py @@ -23,7 +23,7 @@ def is_datafile_valid(datafile): - """ Given a datafile determine if it is valid or not. + """ Given a datafile determine if it is valid or not. Args: datafile: JSON string representing the project. @@ -32,21 +32,21 @@ def is_datafile_valid(datafile): Boolean depending upon whether datafile is valid or not. """ - try: - datafile_json = json.loads(datafile) - except: - return False + try: + datafile_json = json.loads(datafile) + except: + return False - try: - jsonschema.Draft4Validator(constants.JSON_SCHEMA).validate(datafile_json) - except: - return False + try: + jsonschema.Draft4Validator(constants.JSON_SCHEMA).validate(datafile_json) + except: + return False - return True + return True def _has_method(obj, method): - """ Given an object determine if it supports the method. + """ Given an object determine if it supports the method. Args: obj: Object which needs to be inspected. @@ -56,11 +56,11 @@ def _has_method(obj, method): Boolean depending upon whether the method is available or not. """ - return getattr(obj, method, None) is not None + return getattr(obj, method, None) is not None def is_config_manager_valid(config_manager): - """ Given a config_manager determine if it is valid or not i.e. provides a get_config method. + """ Given a config_manager determine if it is valid or not i.e. provides a get_config method. Args: config_manager: Provides a get_config method to handle exceptions. @@ -69,11 +69,11 @@ def is_config_manager_valid(config_manager): Boolean depending upon whether config_manager is valid or not. """ - return _has_method(config_manager, 'get_config') + return _has_method(config_manager, 'get_config') def is_event_processor_valid(event_processor): - """ Given an event_processor, determine if it is valid or not i.e. provides a process method. + """ Given an event_processor, determine if it is valid or not i.e. provides a process method. Args: event_processor: Provides a process method to create user events and then send requests. @@ -82,11 +82,11 @@ def is_event_processor_valid(event_processor): Boolean depending upon whether event_processor is valid or not. """ - return _has_method(event_processor, 'process') + return _has_method(event_processor, 'process') def is_error_handler_valid(error_handler): - """ Given a error_handler determine if it is valid or not i.e. provides a handle_error method. + """ Given a error_handler determine if it is valid or not i.e. provides a handle_error method. Args: error_handler: Provides a handle_error method to handle exceptions. @@ -95,11 +95,11 @@ def is_error_handler_valid(error_handler): Boolean depending upon whether error_handler is valid or not. """ - return _has_method(error_handler, 'handle_error') + return _has_method(error_handler, 'handle_error') def is_event_dispatcher_valid(event_dispatcher): - """ Given a event_dispatcher determine if it is valid or not i.e. provides a dispatch_event method. + """ Given a event_dispatcher determine if it is valid or not i.e. provides a dispatch_event method. Args: event_dispatcher: Provides a dispatch_event method to send requests. @@ -108,11 +108,11 @@ def is_event_dispatcher_valid(event_dispatcher): Boolean depending upon whether event_dispatcher is valid or not. """ - return _has_method(event_dispatcher, 'dispatch_event') + return _has_method(event_dispatcher, 'dispatch_event') def is_logger_valid(logger): - """ Given a logger determine if it is valid or not i.e. provides a log method. + """ Given a logger determine if it is valid or not i.e. provides a log method. Args: logger: Provides a log method to log messages. @@ -121,11 +121,11 @@ def is_logger_valid(logger): Boolean depending upon whether logger is valid or not. """ - return _has_method(logger, 'log') + return _has_method(logger, 'log') def is_notification_center_valid(notification_center): - """ Given notification_center determine if it is valid or not. + """ Given notification_center determine if it is valid or not. Args: notification_center: Instance of notification_center.NotificationCenter @@ -134,11 +134,11 @@ def is_notification_center_valid(notification_center): Boolean denoting instance is valid or not. """ - return isinstance(notification_center, NotificationCenter) + return isinstance(notification_center, NotificationCenter) def are_attributes_valid(attributes): - """ Determine if attributes provided are dict or not. + """ Determine if attributes provided are dict or not. Args: attributes: User attributes which need to be validated. @@ -147,11 +147,11 @@ def are_attributes_valid(attributes): Boolean depending upon whether attributes are in valid format or not. """ - return type(attributes) is dict + return type(attributes) is dict def are_event_tags_valid(event_tags): - """ Determine if event tags provided are dict or not. + """ Determine if event tags provided are dict or not. Args: event_tags: Event tags which need to be validated. @@ -160,11 +160,11 @@ def are_event_tags_valid(event_tags): Boolean depending upon whether event_tags are in valid format or not. """ - return type(event_tags) is dict + return type(event_tags) is dict def is_user_profile_valid(user_profile): - """ Determine if provided user profile is valid or not. + """ Determine if provided user profile is valid or not. Args: user_profile: User's profile which needs to be validated. @@ -173,31 +173,31 @@ def is_user_profile_valid(user_profile): Boolean depending upon whether profile is valid or not. """ - if not user_profile: - return False + if not user_profile: + return False - if not type(user_profile) is dict: - return False + if not type(user_profile) is dict: + return False - if UserProfile.USER_ID_KEY not in user_profile: - return False + if UserProfile.USER_ID_KEY not in user_profile: + return False - if UserProfile.EXPERIMENT_BUCKET_MAP_KEY not in user_profile: - return False + if UserProfile.EXPERIMENT_BUCKET_MAP_KEY not in user_profile: + return False - experiment_bucket_map = user_profile.get(UserProfile.EXPERIMENT_BUCKET_MAP_KEY) - if not type(experiment_bucket_map) is dict: - return False + experiment_bucket_map = user_profile.get(UserProfile.EXPERIMENT_BUCKET_MAP_KEY) + if not type(experiment_bucket_map) is dict: + return False - for decision in experiment_bucket_map.values(): - if type(decision) is not dict or UserProfile.VARIATION_ID_KEY not in decision: - return False + for decision in experiment_bucket_map.values(): + if type(decision) is not dict or UserProfile.VARIATION_ID_KEY not in decision: + return False - return True + return True def is_non_empty_string(input_id_key): - """ Determine if provided input_id_key is a non-empty string or not. + """ Determine if provided input_id_key is a non-empty string or not. Args: input_id_key: Variable which needs to be validated. @@ -205,14 +205,14 @@ def is_non_empty_string(input_id_key): Returns: Boolean depending upon whether input is valid or not. """ - if input_id_key and isinstance(input_id_key, string_types): - return True + if input_id_key and isinstance(input_id_key, string_types): + return True - return False + return False def is_attribute_valid(attribute_key, attribute_value): - """ Determine if given attribute is valid. + """ Determine if given attribute is valid. Args: attribute_key: Variable which needs to be validated @@ -224,20 +224,20 @@ def is_attribute_valid(attribute_key, attribute_value): True otherwise """ - if not isinstance(attribute_key, string_types): - return False + if not isinstance(attribute_key, string_types): + return False - if isinstance(attribute_value, (string_types, bool)): - return True + if isinstance(attribute_value, (string_types, bool)): + return True - if isinstance(attribute_value, (numbers.Integral, float)): - return is_finite_number(attribute_value) + if isinstance(attribute_value, (numbers.Integral, float)): + return is_finite_number(attribute_value) - return False + return False def is_finite_number(value): - """ Validates if the given value is a number, enforces + """ Validates if the given value is a number, enforces absolute limit of 2^53 and restricts NAN, INF, -INF. Args: @@ -247,26 +247,26 @@ def is_finite_number(value): Boolean: True if value is a number and not NAN, INF, -INF or greater than absolute limit of 2^53 else False. """ - if not isinstance(value, (numbers.Integral, float)): - # numbers.Integral instead of int to accommodate long integer in python 2 - return False + if not isinstance(value, (numbers.Integral, float)): + # numbers.Integral instead of int to accommodate long integer in python 2 + return False - if isinstance(value, bool): - # bool is a subclass of int - return False + if isinstance(value, bool): + # bool is a subclass of int + return False - if isinstance(value, float): - if math.isnan(value) or math.isinf(value): - return False + if isinstance(value, float): + if math.isnan(value) or math.isinf(value): + return False - if abs(value) > (2**53): - return False + if abs(value) > (2 ** 53): + return False - return True + return True def are_values_same_type(first_val, second_val): - """ Method to verify that both values belong to same type. Float and integer are + """ Method to verify that both values belong to same type. Float and integer are considered as same type. Args: @@ -277,19 +277,19 @@ def are_values_same_type(first_val, second_val): Boolean: True if both values belong to same type. Otherwise False. """ - first_val_type = type(first_val) - second_val_type = type(second_val) + first_val_type = type(first_val) + second_val_type = type(second_val) - # use isinstance to accomodate Python 2 unicode and str types. - if isinstance(first_val, string_types) and isinstance(second_val, string_types): - return True + # use isinstance to accomodate Python 2 unicode and str types. + if isinstance(first_val, string_types) and isinstance(second_val, string_types): + return True - # Compare types if one of the values is bool because bool is a subclass on Integer. - if isinstance(first_val, bool) or isinstance(second_val, bool): - return first_val_type == second_val_type + # Compare types if one of the values is bool because bool is a subclass on Integer. + if isinstance(first_val, bool) or isinstance(second_val, bool): + return first_val_type == second_val_type - # Treat ints and floats as same type. - if isinstance(first_val, (numbers.Integral, float)) and isinstance(second_val, (numbers.Integral, float)): - return True + # Treat ints and floats as same type. + if isinstance(first_val, (numbers.Integral, float)) and isinstance(second_val, (numbers.Integral, float)): + return True - return False + return False diff --git a/optimizely/lib/pymmh3.py b/optimizely/lib/pymmh3.py index 7fc9eb5c..0f107709 100755 --- a/optimizely/lib/pymmh3.py +++ b/optimizely/lib/pymmh3.py @@ -18,55 +18,61 @@ ''' import sys as _sys -if (_sys.version_info > (3, 0)): - def xrange( a, b, c ): - return range( a, b, c ) + +if _sys.version_info > (3, 0): + + def xrange(a, b, c): + return range(a, b, c) + def xencode(x): if isinstance(x, bytes) or isinstance(x, bytearray): return x else: return x.encode() + + else: + def xencode(x): return x + + del _sys -def hash( key, seed = 0x0 ): + +def hash(key, seed=0x0): ''' Implements 32bit murmur3 hash. ''' - key = bytearray( xencode(key) ) + key = bytearray(xencode(key)) - def fmix( h ): + def fmix(h): h ^= h >> 16 - h = ( h * 0x85ebca6b ) & 0xFFFFFFFF + h = (h * 0x85EBCA6B) & 0xFFFFFFFF h ^= h >> 13 - h = ( h * 0xc2b2ae35 ) & 0xFFFFFFFF + h = (h * 0xC2B2AE35) & 0xFFFFFFFF h ^= h >> 16 return h - length = len( key ) - nblocks = int( length / 4 ) + length = len(key) + nblocks = int(length / 4) h1 = seed - c1 = 0xcc9e2d51 - c2 = 0x1b873593 + c1 = 0xCC9E2D51 + c2 = 0x1B873593 # body - for block_start in xrange( 0, nblocks * 4, 4 ): + for block_start in xrange(0, nblocks * 4, 4): # ??? big endian? - k1 = key[ block_start + 3 ] << 24 | \ - key[ block_start + 2 ] << 16 | \ - key[ block_start + 1 ] << 8 | \ - key[ block_start + 0 ] - - k1 = ( c1 * k1 ) & 0xFFFFFFFF - k1 = ( k1 << 15 | k1 >> 17 ) & 0xFFFFFFFF # inlined ROTL32 - k1 = ( c2 * k1 ) & 0xFFFFFFFF - + k1 = key[block_start + 3] << 24 | key[block_start + 2] << 16 | key[block_start + 1] << 8 | key[block_start + 0] + + k1 = (c1 * k1) & 0xFFFFFFFF + k1 = (k1 << 15 | k1 >> 17) & 0xFFFFFFFF # inlined ROTL32 + k1 = (c2 * k1) & 0xFFFFFFFF + h1 ^= k1 - h1 = ( h1 << 13 | h1 >> 19 ) & 0xFFFFFFFF # inlined ROTL32 - h1 = ( h1 * 5 + 0xe6546b64 ) & 0xFFFFFFFF + h1 = (h1 << 13 | h1 >> 19) & 0xFFFFFFFF # inlined ROTL32 + h1 = (h1 * 5 + 0xE6546B64) & 0xFFFFFFFF # tail tail_index = nblocks * 4 @@ -74,235 +80,248 @@ def fmix( h ): tail_size = length & 3 if tail_size >= 3: - k1 ^= key[ tail_index + 2 ] << 16 + k1 ^= key[tail_index + 2] << 16 if tail_size >= 2: - k1 ^= key[ tail_index + 1 ] << 8 + k1 ^= key[tail_index + 1] << 8 if tail_size >= 1: - k1 ^= key[ tail_index + 0 ] - + k1 ^= key[tail_index + 0] + if tail_size > 0: - k1 = ( k1 * c1 ) & 0xFFFFFFFF - k1 = ( k1 << 15 | k1 >> 17 ) & 0xFFFFFFFF # inlined ROTL32 - k1 = ( k1 * c2 ) & 0xFFFFFFFF + k1 = (k1 * c1) & 0xFFFFFFFF + k1 = (k1 << 15 | k1 >> 17) & 0xFFFFFFFF # inlined ROTL32 + k1 = (k1 * c2) & 0xFFFFFFFF h1 ^= k1 - #finalization - unsigned_val = fmix( h1 ^ length ) + # finalization + unsigned_val = fmix(h1 ^ length) if unsigned_val & 0x80000000 == 0: return unsigned_val else: - return -( (unsigned_val ^ 0xFFFFFFFF) + 1 ) + return -((unsigned_val ^ 0xFFFFFFFF) + 1) -def hash128( key, seed = 0x0, x64arch = True ): +def hash128(key, seed=0x0, x64arch=True): ''' Implements 128bit murmur3 hash. ''' - def hash128_x64( key, seed ): + + def hash128_x64(key, seed): ''' Implements 128bit murmur3 hash for x64. ''' - def fmix( k ): + def fmix(k): k ^= k >> 33 - k = ( k * 0xff51afd7ed558ccd ) & 0xFFFFFFFFFFFFFFFF + k = (k * 0xFF51AFD7ED558CCD) & 0xFFFFFFFFFFFFFFFF k ^= k >> 33 - k = ( k * 0xc4ceb9fe1a85ec53 ) & 0xFFFFFFFFFFFFFFFF + k = (k * 0xC4CEB9FE1A85EC53) & 0xFFFFFFFFFFFFFFFF k ^= k >> 33 return k - length = len( key ) - nblocks = int( length / 16 ) + length = len(key) + nblocks = int(length / 16) h1 = seed h2 = seed - c1 = 0x87c37b91114253d5 - c2 = 0x4cf5ad432745937f + c1 = 0x87C37B91114253D5 + c2 = 0x4CF5AD432745937F - #body - for block_start in xrange( 0, nblocks * 8, 8 ): + # body + for block_start in xrange(0, nblocks * 8, 8): # ??? big endian? - k1 = key[ 2 * block_start + 7 ] << 56 | \ - key[ 2 * block_start + 6 ] << 48 | \ - key[ 2 * block_start + 5 ] << 40 | \ - key[ 2 * block_start + 4 ] << 32 | \ - key[ 2 * block_start + 3 ] << 24 | \ - key[ 2 * block_start + 2 ] << 16 | \ - key[ 2 * block_start + 1 ] << 8 | \ - key[ 2 * block_start + 0 ] - - k2 = key[ 2 * block_start + 15 ] << 56 | \ - key[ 2 * block_start + 14 ] << 48 | \ - key[ 2 * block_start + 13 ] << 40 | \ - key[ 2 * block_start + 12 ] << 32 | \ - key[ 2 * block_start + 11 ] << 24 | \ - key[ 2 * block_start + 10 ] << 16 | \ - key[ 2 * block_start + 9 ] << 8 | \ - key[ 2 * block_start + 8 ] - - k1 = ( c1 * k1 ) & 0xFFFFFFFFFFFFFFFF - k1 = ( k1 << 31 | k1 >> 33 ) & 0xFFFFFFFFFFFFFFFF # inlined ROTL64 - k1 = ( c2 * k1 ) & 0xFFFFFFFFFFFFFFFF + k1 = ( + key[2 * block_start + 7] << 56 + | key[2 * block_start + 6] << 48 + | key[2 * block_start + 5] << 40 + | key[2 * block_start + 4] << 32 + | key[2 * block_start + 3] << 24 + | key[2 * block_start + 2] << 16 + | key[2 * block_start + 1] << 8 + | key[2 * block_start + 0] + ) + + k2 = ( + key[2 * block_start + 15] << 56 + | key[2 * block_start + 14] << 48 + | key[2 * block_start + 13] << 40 + | key[2 * block_start + 12] << 32 + | key[2 * block_start + 11] << 24 + | key[2 * block_start + 10] << 16 + | key[2 * block_start + 9] << 8 + | key[2 * block_start + 8] + ) + + k1 = (c1 * k1) & 0xFFFFFFFFFFFFFFFF + k1 = (k1 << 31 | k1 >> 33) & 0xFFFFFFFFFFFFFFFF # inlined ROTL64 + k1 = (c2 * k1) & 0xFFFFFFFFFFFFFFFF h1 ^= k1 - h1 = ( h1 << 27 | h1 >> 37 ) & 0xFFFFFFFFFFFFFFFF # inlined ROTL64 - h1 = ( h1 + h2 ) & 0xFFFFFFFFFFFFFFFF - h1 = ( h1 * 5 + 0x52dce729 ) & 0xFFFFFFFFFFFFFFFF + h1 = (h1 << 27 | h1 >> 37) & 0xFFFFFFFFFFFFFFFF # inlined ROTL64 + h1 = (h1 + h2) & 0xFFFFFFFFFFFFFFFF + h1 = (h1 * 5 + 0x52DCE729) & 0xFFFFFFFFFFFFFFFF - k2 = ( c2 * k2 ) & 0xFFFFFFFFFFFFFFFF - k2 = ( k2 << 33 | k2 >> 31 ) & 0xFFFFFFFFFFFFFFFF # inlined ROTL64 - k2 = ( c1 * k2 ) & 0xFFFFFFFFFFFFFFFF + k2 = (c2 * k2) & 0xFFFFFFFFFFFFFFFF + k2 = (k2 << 33 | k2 >> 31) & 0xFFFFFFFFFFFFFFFF # inlined ROTL64 + k2 = (c1 * k2) & 0xFFFFFFFFFFFFFFFF h2 ^= k2 - h2 = ( h2 << 31 | h2 >> 33 ) & 0xFFFFFFFFFFFFFFFF # inlined ROTL64 - h2 = ( h1 + h2 ) & 0xFFFFFFFFFFFFFFFF - h2 = ( h2 * 5 + 0x38495ab5 ) & 0xFFFFFFFFFFFFFFFF + h2 = (h2 << 31 | h2 >> 33) & 0xFFFFFFFFFFFFFFFF # inlined ROTL64 + h2 = (h1 + h2) & 0xFFFFFFFFFFFFFFFF + h2 = (h2 * 5 + 0x38495AB5) & 0xFFFFFFFFFFFFFFFF - #tail + # tail tail_index = nblocks * 16 k1 = 0 k2 = 0 tail_size = length & 15 if tail_size >= 15: - k2 ^= key[ tail_index + 14 ] << 48 + k2 ^= key[tail_index + 14] << 48 if tail_size >= 14: - k2 ^= key[ tail_index + 13 ] << 40 + k2 ^= key[tail_index + 13] << 40 if tail_size >= 13: - k2 ^= key[ tail_index + 12 ] << 32 + k2 ^= key[tail_index + 12] << 32 if tail_size >= 12: - k2 ^= key[ tail_index + 11 ] << 24 + k2 ^= key[tail_index + 11] << 24 if tail_size >= 11: - k2 ^= key[ tail_index + 10 ] << 16 + k2 ^= key[tail_index + 10] << 16 if tail_size >= 10: - k2 ^= key[ tail_index + 9 ] << 8 - if tail_size >= 9: - k2 ^= key[ tail_index + 8 ] + k2 ^= key[tail_index + 9] << 8 + if tail_size >= 9: + k2 ^= key[tail_index + 8] if tail_size > 8: - k2 = ( k2 * c2 ) & 0xFFFFFFFFFFFFFFFF - k2 = ( k2 << 33 | k2 >> 31 ) & 0xFFFFFFFFFFFFFFFF # inlined ROTL64 - k2 = ( k2 * c1 ) & 0xFFFFFFFFFFFFFFFF + k2 = (k2 * c2) & 0xFFFFFFFFFFFFFFFF + k2 = (k2 << 33 | k2 >> 31) & 0xFFFFFFFFFFFFFFFF # inlined ROTL64 + k2 = (k2 * c1) & 0xFFFFFFFFFFFFFFFF h2 ^= k2 - if tail_size >= 8: - k1 ^= key[ tail_index + 7 ] << 56 - if tail_size >= 7: - k1 ^= key[ tail_index + 6 ] << 48 - if tail_size >= 6: - k1 ^= key[ tail_index + 5 ] << 40 - if tail_size >= 5: - k1 ^= key[ tail_index + 4 ] << 32 - if tail_size >= 4: - k1 ^= key[ tail_index + 3 ] << 24 - if tail_size >= 3: - k1 ^= key[ tail_index + 2 ] << 16 - if tail_size >= 2: - k1 ^= key[ tail_index + 1 ] << 8 - if tail_size >= 1: - k1 ^= key[ tail_index + 0 ] + if tail_size >= 8: + k1 ^= key[tail_index + 7] << 56 + if tail_size >= 7: + k1 ^= key[tail_index + 6] << 48 + if tail_size >= 6: + k1 ^= key[tail_index + 5] << 40 + if tail_size >= 5: + k1 ^= key[tail_index + 4] << 32 + if tail_size >= 4: + k1 ^= key[tail_index + 3] << 24 + if tail_size >= 3: + k1 ^= key[tail_index + 2] << 16 + if tail_size >= 2: + k1 ^= key[tail_index + 1] << 8 + if tail_size >= 1: + k1 ^= key[tail_index + 0] if tail_size > 0: - k1 = ( k1 * c1 ) & 0xFFFFFFFFFFFFFFFF - k1 = ( k1 << 31 | k1 >> 33 ) & 0xFFFFFFFFFFFFFFFF # inlined ROTL64 - k1 = ( k1 * c2 ) & 0xFFFFFFFFFFFFFFFF + k1 = (k1 * c1) & 0xFFFFFFFFFFFFFFFF + k1 = (k1 << 31 | k1 >> 33) & 0xFFFFFFFFFFFFFFFF # inlined ROTL64 + k1 = (k1 * c2) & 0xFFFFFFFFFFFFFFFF h1 ^= k1 - #finalization + # finalization h1 ^= length h2 ^= length - h1 = ( h1 + h2 ) & 0xFFFFFFFFFFFFFFFF - h2 = ( h1 + h2 ) & 0xFFFFFFFFFFFFFFFF + h1 = (h1 + h2) & 0xFFFFFFFFFFFFFFFF + h2 = (h1 + h2) & 0xFFFFFFFFFFFFFFFF - h1 = fmix( h1 ) - h2 = fmix( h2 ) + h1 = fmix(h1) + h2 = fmix(h2) - h1 = ( h1 + h2 ) & 0xFFFFFFFFFFFFFFFF - h2 = ( h1 + h2 ) & 0xFFFFFFFFFFFFFFFF + h1 = (h1 + h2) & 0xFFFFFFFFFFFFFFFF + h2 = (h1 + h2) & 0xFFFFFFFFFFFFFFFF - return ( h2 << 64 | h1 ) + return h2 << 64 | h1 - def hash128_x86( key, seed ): + def hash128_x86(key, seed): ''' Implements 128bit murmur3 hash for x86. ''' - def fmix( h ): + def fmix(h): h ^= h >> 16 - h = ( h * 0x85ebca6b ) & 0xFFFFFFFF + h = (h * 0x85EBCA6B) & 0xFFFFFFFF h ^= h >> 13 - h = ( h * 0xc2b2ae35 ) & 0xFFFFFFFF + h = (h * 0xC2B2AE35) & 0xFFFFFFFF h ^= h >> 16 return h - length = len( key ) - nblocks = int( length / 16 ) + length = len(key) + nblocks = int(length / 16) h1 = seed h2 = seed h3 = seed h4 = seed - c1 = 0x239b961b - c2 = 0xab0e9789 - c3 = 0x38b34ae5 - c4 = 0xa1e38b93 - - #body - for block_start in xrange( 0, nblocks * 16, 16 ): - k1 = key[ block_start + 3 ] << 24 | \ - key[ block_start + 2 ] << 16 | \ - key[ block_start + 1 ] << 8 | \ - key[ block_start + 0 ] - - k2 = key[ block_start + 7 ] << 24 | \ - key[ block_start + 6 ] << 16 | \ - key[ block_start + 5 ] << 8 | \ - key[ block_start + 4 ] - - k3 = key[ block_start + 11 ] << 24 | \ - key[ block_start + 10 ] << 16 | \ - key[ block_start + 9 ] << 8 | \ - key[ block_start + 8 ] - - k4 = key[ block_start + 15 ] << 24 | \ - key[ block_start + 14 ] << 16 | \ - key[ block_start + 13 ] << 8 | \ - key[ block_start + 12 ] - - k1 = ( c1 * k1 ) & 0xFFFFFFFF - k1 = ( k1 << 15 | k1 >> 17 ) & 0xFFFFFFFF # inlined ROTL32 - k1 = ( c2 * k1 ) & 0xFFFFFFFF + c1 = 0x239B961B + c2 = 0xAB0E9789 + c3 = 0x38B34AE5 + c4 = 0xA1E38B93 + + # body + for block_start in xrange(0, nblocks * 16, 16): + k1 = ( + key[block_start + 3] << 24 + | key[block_start + 2] << 16 + | key[block_start + 1] << 8 + | key[block_start + 0] + ) + + k2 = ( + key[block_start + 7] << 24 + | key[block_start + 6] << 16 + | key[block_start + 5] << 8 + | key[block_start + 4] + ) + + k3 = ( + key[block_start + 11] << 24 + | key[block_start + 10] << 16 + | key[block_start + 9] << 8 + | key[block_start + 8] + ) + + k4 = ( + key[block_start + 15] << 24 + | key[block_start + 14] << 16 + | key[block_start + 13] << 8 + | key[block_start + 12] + ) + + k1 = (c1 * k1) & 0xFFFFFFFF + k1 = (k1 << 15 | k1 >> 17) & 0xFFFFFFFF # inlined ROTL32 + k1 = (c2 * k1) & 0xFFFFFFFF h1 ^= k1 - h1 = ( h1 << 19 | h1 >> 13 ) & 0xFFFFFFFF # inlined ROTL32 - h1 = ( h1 + h2 ) & 0xFFFFFFFF - h1 = ( h1 * 5 + 0x561ccd1b ) & 0xFFFFFFFF + h1 = (h1 << 19 | h1 >> 13) & 0xFFFFFFFF # inlined ROTL32 + h1 = (h1 + h2) & 0xFFFFFFFF + h1 = (h1 * 5 + 0x561CCD1B) & 0xFFFFFFFF - k2 = ( c2 * k2 ) & 0xFFFFFFFF - k2 = ( k2 << 16 | k2 >> 16 ) & 0xFFFFFFFF # inlined ROTL32 - k2 = ( c3 * k2 ) & 0xFFFFFFFF + k2 = (c2 * k2) & 0xFFFFFFFF + k2 = (k2 << 16 | k2 >> 16) & 0xFFFFFFFF # inlined ROTL32 + k2 = (c3 * k2) & 0xFFFFFFFF h2 ^= k2 - h2 = ( h2 << 17 | h2 >> 15 ) & 0xFFFFFFFF # inlined ROTL32 - h2 = ( h2 + h3 ) & 0xFFFFFFFF - h2 = ( h2 * 5 + 0x0bcaa747 ) & 0xFFFFFFFF + h2 = (h2 << 17 | h2 >> 15) & 0xFFFFFFFF # inlined ROTL32 + h2 = (h2 + h3) & 0xFFFFFFFF + h2 = (h2 * 5 + 0x0BCAA747) & 0xFFFFFFFF - k3 = ( c3 * k3 ) & 0xFFFFFFFF - k3 = ( k3 << 17 | k3 >> 15 ) & 0xFFFFFFFF # inlined ROTL32 - k3 = ( c4 * k3 ) & 0xFFFFFFFF + k3 = (c3 * k3) & 0xFFFFFFFF + k3 = (k3 << 17 | k3 >> 15) & 0xFFFFFFFF # inlined ROTL32 + k3 = (c4 * k3) & 0xFFFFFFFF h3 ^= k3 - h3 = ( h3 << 15 | h3 >> 17 ) & 0xFFFFFFFF # inlined ROTL32 - h3 = ( h3 + h4 ) & 0xFFFFFFFF - h3 = ( h3 * 5 + 0x96cd1c35 ) & 0xFFFFFFFF + h3 = (h3 << 15 | h3 >> 17) & 0xFFFFFFFF # inlined ROTL32 + h3 = (h3 + h4) & 0xFFFFFFFF + h3 = (h3 * 5 + 0x96CD1C35) & 0xFFFFFFFF - k4 = ( c4 * k4 ) & 0xFFFFFFFF - k4 = ( k4 << 18 | k4 >> 14 ) & 0xFFFFFFFF # inlined ROTL32 - k4 = ( c1 * k4 ) & 0xFFFFFFFF + k4 = (c4 * k4) & 0xFFFFFFFF + k4 = (k4 << 18 | k4 >> 14) & 0xFFFFFFFF # inlined ROTL32 + k4 = (c1 * k4) & 0xFFFFFFFF h4 ^= k4 - h4 = ( h4 << 13 | h4 >> 19 ) & 0xFFFFFFFF # inlined ROTL32 - h4 = ( h1 + h4 ) & 0xFFFFFFFF - h4 = ( h4 * 5 + 0x32ac3b17 ) & 0xFFFFFFFF + h4 = (h4 << 13 | h4 >> 19) & 0xFFFFFFFF # inlined ROTL32 + h4 = (h1 + h4) & 0xFFFFFFFF + h4 = (h4 * 5 + 0x32AC3B17) & 0xFFFFFFFF - #tail + # tail tail_index = nblocks * 16 k1 = 0 k2 = 0 @@ -311,128 +330,128 @@ def fmix( h ): tail_size = length & 15 if tail_size >= 15: - k4 ^= key[ tail_index + 14 ] << 16 + k4 ^= key[tail_index + 14] << 16 if tail_size >= 14: - k4 ^= key[ tail_index + 13 ] << 8 + k4 ^= key[tail_index + 13] << 8 if tail_size >= 13: - k4 ^= key[ tail_index + 12 ] + k4 ^= key[tail_index + 12] if tail_size > 12: - k4 = ( k4 * c4 ) & 0xFFFFFFFF - k4 = ( k4 << 18 | k4 >> 14 ) & 0xFFFFFFFF # inlined ROTL32 - k4 = ( k4 * c1 ) & 0xFFFFFFFF + k4 = (k4 * c4) & 0xFFFFFFFF + k4 = (k4 << 18 | k4 >> 14) & 0xFFFFFFFF # inlined ROTL32 + k4 = (k4 * c1) & 0xFFFFFFFF h4 ^= k4 if tail_size >= 12: - k3 ^= key[ tail_index + 11 ] << 24 + k3 ^= key[tail_index + 11] << 24 if tail_size >= 11: - k3 ^= key[ tail_index + 10 ] << 16 + k3 ^= key[tail_index + 10] << 16 if tail_size >= 10: - k3 ^= key[ tail_index + 9 ] << 8 - if tail_size >= 9: - k3 ^= key[ tail_index + 8 ] + k3 ^= key[tail_index + 9] << 8 + if tail_size >= 9: + k3 ^= key[tail_index + 8] if tail_size > 8: - k3 = ( k3 * c3 ) & 0xFFFFFFFF - k3 = ( k3 << 17 | k3 >> 15 ) & 0xFFFFFFFF # inlined ROTL32 - k3 = ( k3 * c4 ) & 0xFFFFFFFF + k3 = (k3 * c3) & 0xFFFFFFFF + k3 = (k3 << 17 | k3 >> 15) & 0xFFFFFFFF # inlined ROTL32 + k3 = (k3 * c4) & 0xFFFFFFFF h3 ^= k3 if tail_size >= 8: - k2 ^= key[ tail_index + 7 ] << 24 + k2 ^= key[tail_index + 7] << 24 if tail_size >= 7: - k2 ^= key[ tail_index + 6 ] << 16 + k2 ^= key[tail_index + 6] << 16 if tail_size >= 6: - k2 ^= key[ tail_index + 5 ] << 8 + k2 ^= key[tail_index + 5] << 8 if tail_size >= 5: - k2 ^= key[ tail_index + 4 ] + k2 ^= key[tail_index + 4] if tail_size > 4: - k2 = ( k2 * c2 ) & 0xFFFFFFFF - k2 = ( k2 << 16 | k2 >> 16 ) & 0xFFFFFFFF # inlined ROTL32 - k2 = ( k2 * c3 ) & 0xFFFFFFFF + k2 = (k2 * c2) & 0xFFFFFFFF + k2 = (k2 << 16 | k2 >> 16) & 0xFFFFFFFF # inlined ROTL32 + k2 = (k2 * c3) & 0xFFFFFFFF h2 ^= k2 if tail_size >= 4: - k1 ^= key[ tail_index + 3 ] << 24 + k1 ^= key[tail_index + 3] << 24 if tail_size >= 3: - k1 ^= key[ tail_index + 2 ] << 16 + k1 ^= key[tail_index + 2] << 16 if tail_size >= 2: - k1 ^= key[ tail_index + 1 ] << 8 + k1 ^= key[tail_index + 1] << 8 if tail_size >= 1: - k1 ^= key[ tail_index + 0 ] + k1 ^= key[tail_index + 0] if tail_size > 0: - k1 = ( k1 * c1 ) & 0xFFFFFFFF - k1 = ( k1 << 15 | k1 >> 17 ) & 0xFFFFFFFF # inlined ROTL32 - k1 = ( k1 * c2 ) & 0xFFFFFFFF + k1 = (k1 * c1) & 0xFFFFFFFF + k1 = (k1 << 15 | k1 >> 17) & 0xFFFFFFFF # inlined ROTL32 + k1 = (k1 * c2) & 0xFFFFFFFF h1 ^= k1 - #finalization + # finalization h1 ^= length h2 ^= length h3 ^= length h4 ^= length - h1 = ( h1 + h2 ) & 0xFFFFFFFF - h1 = ( h1 + h3 ) & 0xFFFFFFFF - h1 = ( h1 + h4 ) & 0xFFFFFFFF - h2 = ( h1 + h2 ) & 0xFFFFFFFF - h3 = ( h1 + h3 ) & 0xFFFFFFFF - h4 = ( h1 + h4 ) & 0xFFFFFFFF + h1 = (h1 + h2) & 0xFFFFFFFF + h1 = (h1 + h3) & 0xFFFFFFFF + h1 = (h1 + h4) & 0xFFFFFFFF + h2 = (h1 + h2) & 0xFFFFFFFF + h3 = (h1 + h3) & 0xFFFFFFFF + h4 = (h1 + h4) & 0xFFFFFFFF - h1 = fmix( h1 ) - h2 = fmix( h2 ) - h3 = fmix( h3 ) - h4 = fmix( h4 ) + h1 = fmix(h1) + h2 = fmix(h2) + h3 = fmix(h3) + h4 = fmix(h4) - h1 = ( h1 + h2 ) & 0xFFFFFFFF - h1 = ( h1 + h3 ) & 0xFFFFFFFF - h1 = ( h1 + h4 ) & 0xFFFFFFFF - h2 = ( h1 + h2 ) & 0xFFFFFFFF - h3 = ( h1 + h3 ) & 0xFFFFFFFF - h4 = ( h1 + h4 ) & 0xFFFFFFFF + h1 = (h1 + h2) & 0xFFFFFFFF + h1 = (h1 + h3) & 0xFFFFFFFF + h1 = (h1 + h4) & 0xFFFFFFFF + h2 = (h1 + h2) & 0xFFFFFFFF + h3 = (h1 + h3) & 0xFFFFFFFF + h4 = (h1 + h4) & 0xFFFFFFFF - return ( h4 << 96 | h3 << 64 | h2 << 32 | h1 ) + return h4 << 96 | h3 << 64 | h2 << 32 | h1 - key = bytearray( xencode(key) ) + key = bytearray(xencode(key)) if x64arch: - return hash128_x64( key, seed ) + return hash128_x64(key, seed) else: - return hash128_x86( key, seed ) + return hash128_x86(key, seed) -def hash64( key, seed = 0x0, x64arch = True ): +def hash64(key, seed=0x0, x64arch=True): ''' Implements 64bit murmur3 hash. Returns a tuple. ''' - hash_128 = hash128( key, seed, x64arch ) + hash_128 = hash128(key, seed, x64arch) unsigned_val1 = hash_128 & 0xFFFFFFFFFFFFFFFF if unsigned_val1 & 0x8000000000000000 == 0: signed_val1 = unsigned_val1 else: - signed_val1 = -( (unsigned_val1 ^ 0xFFFFFFFFFFFFFFFF) + 1 ) + signed_val1 = -((unsigned_val1 ^ 0xFFFFFFFFFFFFFFFF) + 1) - unsigned_val2 = ( hash_128 >> 64 ) & 0xFFFFFFFFFFFFFFFF + unsigned_val2 = (hash_128 >> 64) & 0xFFFFFFFFFFFFFFFF if unsigned_val2 & 0x8000000000000000 == 0: signed_val2 = unsigned_val2 else: - signed_val2 = -( (unsigned_val2 ^ 0xFFFFFFFFFFFFFFFF) + 1 ) + signed_val2 = -((unsigned_val2 ^ 0xFFFFFFFFFFFFFFFF) + 1) - return ( int( signed_val1 ), int( signed_val2 ) ) + return (int(signed_val1), int(signed_val2)) -def hash_bytes( key, seed = 0x0, x64arch = True ): +def hash_bytes(key, seed=0x0, x64arch=True): ''' Implements 128bit murmur3 hash. Returns a byte string. ''' - hash_128 = hash128( key, seed, x64arch ) + hash_128 = hash128(key, seed, x64arch) bytestring = '' for i in xrange(0, 16, 1): lsbyte = hash_128 & 0xFF - bytestring = bytestring + str( chr( lsbyte ) ) + bytestring = bytestring + str(chr(lsbyte)) hash_128 = hash_128 >> 8 return bytestring @@ -440,12 +459,12 @@ def hash_bytes( key, seed = 0x0, x64arch = True ): if __name__ == "__main__": import argparse - - parser = argparse.ArgumentParser( 'pymurmur3', 'pymurmur [options] "string to hash"' ) - parser.add_argument( '--seed', type = int, default = 0 ) - parser.add_argument( 'strings', default = [], nargs='+') - + + parser = argparse.ArgumentParser('pymurmur3', 'pymurmur [options] "string to hash"') + parser.add_argument('--seed', type=int, default=0) + parser.add_argument('strings', default=[], nargs='+') + opts = parser.parse_args() - + for str_to_hash in opts.strings: - sys.stdout.write( '"%s" = 0x%08X\n' % ( str_to_hash, hash( str_to_hash ) ) ) \ No newline at end of file + sys.stdout.write('"%s" = 0x%08X\n' % (str_to_hash, hash(str_to_hash))) diff --git a/optimizely/logger.py b/optimizely/logger.py index 9530b132..4754e347 100644 --- a/optimizely/logger.py +++ b/optimizely/logger.py @@ -20,7 +20,7 @@ def reset_logger(name, level=None, handler=None): - """ + """ Make a standard python logger object with default formatter, handler, etc. Defaults are: @@ -35,65 +35,59 @@ def reset_logger(name, level=None, handler=None): Returns: a standard python logger with a single handler. """ - # Make the logger and set its level. - if level is None: - level = logging.INFO - logger = logging.getLogger(name) - logger.setLevel(level) - - # Make the handler and attach it. - handler = handler or logging.StreamHandler() - handler.setFormatter(logging.Formatter(_DEFAULT_LOG_FORMAT)) - - # We don't use ``.addHandler``, since this logger may have already been - # instantiated elsewhere with a different handler. It should only ever - # have one, not many. - logger.handlers = [handler] - return logger + # Make the logger and set its level. + if level is None: + level = logging.INFO + logger = logging.getLogger(name) + logger.setLevel(level) + + # Make the handler and attach it. + handler = handler or logging.StreamHandler() + handler.setFormatter(logging.Formatter(_DEFAULT_LOG_FORMAT)) + + # We don't use ``.addHandler``, since this logger may have already been + # instantiated elsewhere with a different handler. It should only ever + # have one, not many. + logger.handlers = [handler] + return logger class BaseLogger(object): - """ Class encapsulating logging functionality. Override with your own logger providing log method. """ + """ Class encapsulating logging functionality. Override with your own logger providing log method. """ - @staticmethod - def log(*args): - pass # pragma: no cover + @staticmethod + def log(*args): + pass # pragma: no cover class NoOpLogger(BaseLogger): - """ Class providing log method which logs nothing. """ - def __init__(self): - self.logger = reset_logger( - name='.'.join([__name__, self.__class__.__name__]), - level=logging.NOTSET, - handler=logging.NullHandler() - ) + """ Class providing log method which logs nothing. """ + + def __init__(self): + self.logger = reset_logger( + name='.'.join([__name__, self.__class__.__name__]), level=logging.NOTSET, handler=logging.NullHandler(), + ) class SimpleLogger(BaseLogger): - """ Class providing log method which logs to stdout. """ + """ Class providing log method which logs to stdout. """ - def __init__(self, min_level=enums.LogLevels.INFO): - self.level = min_level - self.logger = reset_logger( - name='.'.join([__name__, self.__class__.__name__]), - level=min_level - ) + def __init__(self, min_level=enums.LogLevels.INFO): + self.level = min_level + self.logger = reset_logger(name='.'.join([__name__, self.__class__.__name__]), level=min_level) - def log(self, log_level, message): - # Log a deprecation/runtime warning. - # Clients should be using standard loggers instead of this wrapper. - warning = '{} is deprecated. Please use standard python loggers.'.format( - self.__class__ - ) - warnings.warn(warning, DeprecationWarning) + def log(self, log_level, message): + # Log a deprecation/runtime warning. + # Clients should be using standard loggers instead of this wrapper. + warning = '{} is deprecated. Please use standard python loggers.'.format(self.__class__) + warnings.warn(warning, DeprecationWarning) - # Log the message. - self.logger.log(log_level, message) + # Log the message. + self.logger.log(log_level, message) def adapt_logger(logger): - """ + """ Adapt our custom logger.BaseLogger object into a standard logging.Logger object. Adaptations are: @@ -106,12 +100,12 @@ def adapt_logger(logger): Returns: a standard python logging.Logger. """ - if isinstance(logger, logging.Logger): - return logger + if isinstance(logger, logging.Logger): + return logger - # Use the standard python logger created by these classes. - if isinstance(logger, (SimpleLogger, NoOpLogger)): - return logger.logger + # Use the standard python logger created by these classes. + if isinstance(logger, (SimpleLogger, NoOpLogger)): + return logger.logger - # Otherwise, return whatever we were given because we can't adapt. - return logger + # Otherwise, return whatever we were given because we can't adapt. + return logger diff --git a/optimizely/notification_center.py b/optimizely/notification_center.py index 02eefd96..539088a8 100644 --- a/optimizely/notification_center.py +++ b/optimizely/notification_center.py @@ -15,24 +15,24 @@ from . import logger as optimizely_logger -NOTIFICATION_TYPES = tuple(getattr(enums.NotificationTypes, attr) - for attr in dir(enums.NotificationTypes) - if not attr.startswith('__')) +NOTIFICATION_TYPES = tuple( + getattr(enums.NotificationTypes, attr) for attr in dir(enums.NotificationTypes) if not attr.startswith('__') +) class NotificationCenter(object): - """ Class encapsulating methods to manage notifications and their listeners. + """ Class encapsulating methods to manage notifications and their listeners. The enums.NotificationTypes includes predefined notifications.""" - def __init__(self, logger=None): - self.listener_id = 1 - self.notification_listeners = {} - for notification_type in NOTIFICATION_TYPES: - self.notification_listeners[notification_type] = [] - self.logger = optimizely_logger.adapt_logger(logger or optimizely_logger.NoOpLogger()) + def __init__(self, logger=None): + self.listener_id = 1 + self.notification_listeners = {} + for notification_type in NOTIFICATION_TYPES: + self.notification_listeners[notification_type] = [] + self.logger = optimizely_logger.adapt_logger(logger or optimizely_logger.NoOpLogger()) - def add_notification_listener(self, notification_type, notification_callback): - """ Add a notification callback to the notification center for a given notification type. + def add_notification_listener(self, notification_type, notification_callback): + """ Add a notification callback to the notification center for a given notification type. Args: notification_type: A string representing the notification type from helpers.enums.NotificationTypes @@ -44,23 +44,23 @@ def add_notification_listener(self, notification_type, notification_callback): if the notification type is invalid. """ - if notification_type not in NOTIFICATION_TYPES: - self.logger.error('Invalid notification_type: {} provided. Not adding listener.'.format(notification_type)) - return -1 + if notification_type not in NOTIFICATION_TYPES: + self.logger.error('Invalid notification_type: {} provided. Not adding listener.'.format(notification_type)) + return -1 - for _, listener in self.notification_listeners[notification_type]: - if listener == notification_callback: - self.logger.error('Listener has already been added. Not adding it again.') - return -1 + for _, listener in self.notification_listeners[notification_type]: + if listener == notification_callback: + self.logger.error('Listener has already been added. Not adding it again.') + return -1 - self.notification_listeners[notification_type].append((self.listener_id, notification_callback)) - current_listener_id = self.listener_id - self.listener_id += 1 + self.notification_listeners[notification_type].append((self.listener_id, notification_callback)) + current_listener_id = self.listener_id + self.listener_id += 1 - return current_listener_id + return current_listener_id - def remove_notification_listener(self, notification_id): - """ Remove a previously added notification callback. + def remove_notification_listener(self, notification_id): + """ Remove a previously added notification callback. Args: notification_id: The numeric id passed back from add_notification_listener @@ -69,46 +69,48 @@ def remove_notification_listener(self, notification_id): The function returns boolean true if found and removed, false otherwise. """ - for listener in self.notification_listeners.values(): - listener_to_remove = list(filter(lambda tup: tup[0] == notification_id, listener)) - if len(listener_to_remove) > 0: - listener.remove(listener_to_remove[0]) - return True + for listener in self.notification_listeners.values(): + listener_to_remove = list(filter(lambda tup: tup[0] == notification_id, listener)) + if len(listener_to_remove) > 0: + listener.remove(listener_to_remove[0]) + return True - return False + return False - def clear_notification_listeners(self, notification_type): - """ Remove notification listeners for a certain notification type. + def clear_notification_listeners(self, notification_type): + """ Remove notification listeners for a certain notification type. Args: notification_type: String denoting notification type. """ - if notification_type not in NOTIFICATION_TYPES: - self.logger.error('Invalid notification_type: {} provided. Not removing any listener.'.format(notification_type)) - self.notification_listeners[notification_type] = [] + if notification_type not in NOTIFICATION_TYPES: + self.logger.error( + 'Invalid notification_type: {} provided. Not removing any listener.'.format(notification_type) + ) + self.notification_listeners[notification_type] = [] - def clear_notifications(self, notification_type): - """ (DEPRECATED since 3.2.0, use clear_notification_listeners) + def clear_notifications(self, notification_type): + """ (DEPRECATED since 3.2.0, use clear_notification_listeners) Remove notification listeners for a certain notification type. Args: notification_type: key to the list of notifications .helpers.enums.NotificationTypes """ - self.clear_notification_listeners(notification_type) + self.clear_notification_listeners(notification_type) - def clear_all_notification_listeners(self): - """ Remove all notification listeners. """ - for notification_type in self.notification_listeners.keys(): - self.clear_notification_listeners(notification_type) + def clear_all_notification_listeners(self): + """ Remove all notification listeners. """ + for notification_type in self.notification_listeners.keys(): + self.clear_notification_listeners(notification_type) - def clear_all_notifications(self): - """ (DEPRECATED since 3.2.0, use clear_all_notification_listeners) + def clear_all_notifications(self): + """ (DEPRECATED since 3.2.0, use clear_all_notification_listeners) Remove all notification listeners. """ - self.clear_all_notification_listeners() + self.clear_all_notification_listeners() - def send_notifications(self, notification_type, *args): - """ Fires off the notification for the specific event. Uses var args to pass in a + def send_notifications(self, notification_type, *args): + """ Fires off the notification for the specific event. Uses var args to pass in a arbitrary list of parameter according to which notification type was fired. Args: @@ -116,14 +118,17 @@ def send_notifications(self, notification_type, *args): args: Variable list of arguments to the callback. """ - if notification_type not in NOTIFICATION_TYPES: - self.logger.error('Invalid notification_type: {} provided. ' - 'Not triggering any notification.'.format(notification_type)) - return - - if notification_type in self.notification_listeners: - for notification_id, callback in self.notification_listeners[notification_type]: - try: - callback(*args) - except: - self.logger.exception('Unknown problem when sending "{}" type notification.'.format(notification_type)) + if notification_type not in NOTIFICATION_TYPES: + self.logger.error( + 'Invalid notification_type: {} provided. ' 'Not triggering any notification.'.format(notification_type) + ) + return + + if notification_type in self.notification_listeners: + for notification_id, callback in self.notification_listeners[notification_type]: + try: + callback(*args) + except: + self.logger.exception( + 'Unknown problem when sending "{}" type notification.'.format(notification_type) + ) diff --git a/optimizely/optimizely.py b/optimizely/optimizely.py index a7a860ab..ba82adb8 100644 --- a/optimizely/optimizely.py +++ b/optimizely/optimizely.py @@ -28,20 +28,22 @@ class Optimizely(object): - """ Class encapsulating all SDK functionality. """ - - def __init__(self, - datafile=None, - event_dispatcher=None, - logger=None, - error_handler=None, - skip_json_validation=False, - user_profile_service=None, - sdk_key=None, - config_manager=None, - notification_center=None, - event_processor=None): - """ Optimizely init method for managing Custom projects. + """ Class encapsulating all SDK functionality. """ + + def __init__( + self, + datafile=None, + event_dispatcher=None, + logger=None, + error_handler=None, + skip_json_validation=False, + user_profile_service=None, + sdk_key=None, + config_manager=None, + notification_center=None, + event_processor=None, + ): + """ 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. @@ -63,71 +65,75 @@ def __init__(self, which simply forwards events to the event dispatcher. To enable event batching configure and use optimizely.event.event_processor.BatchEventProcessor. """ - self.logger_name = '.'.join([__name__, self.__class__.__name__]) - self.is_valid = True - self.event_dispatcher = event_dispatcher or default_event_dispatcher - self.logger = _logging.adapt_logger(logger or _logging.NoOpLogger()) - self.error_handler = error_handler or noop_error_handler - self.config_manager = config_manager - self.notification_center = notification_center or NotificationCenter(self.logger) - self.event_processor = event_processor or ForwardingEventProcessor(self.event_dispatcher, - logger=self.logger, - notification_center=self.notification_center) - - try: - self._validate_instantiation_options() - except exceptions.InvalidInputException as error: - self.is_valid = False - # We actually want to log this error to stderr, so make sure the logger - # has a handler capable of doing that. - self.logger = _logging.reset_logger(self.logger_name) - self.logger.exception(str(error)) - return - - if not self.config_manager: - if sdk_key: - self.config_manager = PollingConfigManager(sdk_key=sdk_key, - datafile=datafile, - logger=self.logger, - error_handler=self.error_handler, - notification_center=self.notification_center, - skip_json_validation=skip_json_validation) - else: - self.config_manager = StaticConfigManager(datafile=datafile, - logger=self.logger, - error_handler=self.error_handler, - notification_center=self.notification_center, - skip_json_validation=skip_json_validation) - - self.event_builder = event_builder.EventBuilder() - self.decision_service = decision_service.DecisionService(self.logger, user_profile_service) - - def _validate_instantiation_options(self): - """ Helper method to validate all instantiation parameters. + self.logger_name = '.'.join([__name__, self.__class__.__name__]) + self.is_valid = True + self.event_dispatcher = event_dispatcher or default_event_dispatcher + self.logger = _logging.adapt_logger(logger or _logging.NoOpLogger()) + self.error_handler = error_handler or noop_error_handler + self.config_manager = config_manager + self.notification_center = notification_center or NotificationCenter(self.logger) + self.event_processor = event_processor or ForwardingEventProcessor( + self.event_dispatcher, logger=self.logger, notification_center=self.notification_center, + ) + + try: + self._validate_instantiation_options() + except exceptions.InvalidInputException as error: + self.is_valid = False + # We actually want to log this error to stderr, so make sure the logger + # has a handler capable of doing that. + self.logger = _logging.reset_logger(self.logger_name) + self.logger.exception(str(error)) + return + + if not self.config_manager: + if sdk_key: + self.config_manager = PollingConfigManager( + sdk_key=sdk_key, + datafile=datafile, + logger=self.logger, + error_handler=self.error_handler, + notification_center=self.notification_center, + skip_json_validation=skip_json_validation, + ) + else: + self.config_manager = StaticConfigManager( + datafile=datafile, + logger=self.logger, + error_handler=self.error_handler, + notification_center=self.notification_center, + skip_json_validation=skip_json_validation, + ) + + self.event_builder = event_builder.EventBuilder() + self.decision_service = decision_service.DecisionService(self.logger, user_profile_service) + + def _validate_instantiation_options(self): + """ Helper method to validate all instantiation parameters. 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')) + if self.config_manager and not validator.is_config_manager_valid(self.config_manager): + raise exceptions.InvalidInputException(enums.Errors.INVALID_INPUT.format('config_manager')) - if not validator.is_event_dispatcher_valid(self.event_dispatcher): - raise exceptions.InvalidInputException(enums.Errors.INVALID_INPUT.format('event_dispatcher')) + if not validator.is_event_dispatcher_valid(self.event_dispatcher): + raise exceptions.InvalidInputException(enums.Errors.INVALID_INPUT.format('event_dispatcher')) - if not validator.is_logger_valid(self.logger): - raise exceptions.InvalidInputException(enums.Errors.INVALID_INPUT.format('logger')) + if not validator.is_logger_valid(self.logger): + raise exceptions.InvalidInputException(enums.Errors.INVALID_INPUT.format('logger')) - if not validator.is_error_handler_valid(self.error_handler): - raise exceptions.InvalidInputException(enums.Errors.INVALID_INPUT.format('error_handler')) + if not validator.is_error_handler_valid(self.error_handler): + raise exceptions.InvalidInputException(enums.Errors.INVALID_INPUT.format('error_handler')) - if not validator.is_notification_center_valid(self.notification_center): - raise exceptions.InvalidInputException(enums.Errors.INVALID_INPUT.format('notification_center')) + if not validator.is_notification_center_valid(self.notification_center): + raise exceptions.InvalidInputException(enums.Errors.INVALID_INPUT.format('notification_center')) - if not validator.is_event_processor_valid(self.event_processor): - raise exceptions.InvalidInputException(enums.Errors.INVALID_INPUT.format('event_processor')) + if not validator.is_event_processor_valid(self.event_processor): + raise exceptions.InvalidInputException(enums.Errors.INVALID_INPUT.format('event_processor')) - def _validate_user_inputs(self, attributes=None, event_tags=None): - """ Helper method to validate user inputs. + def _validate_user_inputs(self, attributes=None, event_tags=None): + """ Helper method to validate user inputs. Args: attributes: Dict representing user attributes. @@ -138,20 +144,20 @@ def _validate_user_inputs(self, attributes=None, event_tags=None): """ - if attributes and not validator.are_attributes_valid(attributes): - self.logger.error('Provided attributes are in an invalid format.') - self.error_handler.handle_error(exceptions.InvalidAttributeException(enums.Errors.INVALID_ATTRIBUTE_FORMAT)) - return False + if attributes and not validator.are_attributes_valid(attributes): + self.logger.error('Provided attributes are in an invalid format.') + self.error_handler.handle_error(exceptions.InvalidAttributeException(enums.Errors.INVALID_ATTRIBUTE_FORMAT)) + return False - if event_tags and not validator.are_event_tags_valid(event_tags): - self.logger.error('Provided event tags are in an invalid format.') - self.error_handler.handle_error(exceptions.InvalidEventTagException(enums.Errors.INVALID_EVENT_TAG_FORMAT)) - return False + if event_tags and not validator.are_event_tags_valid(event_tags): + self.logger.error('Provided event tags are in an invalid format.') + self.error_handler.handle_error(exceptions.InvalidEventTagException(enums.Errors.INVALID_EVENT_TAG_FORMAT)) + return False - return True + return True - def _send_impression_event(self, project_config, experiment, variation, user_id, attributes): - """ Helper method to send impression event. + def _send_impression_event(self, project_config, experiment, variation, user_id, attributes): + """ Helper method to send impression event. Args: project_config: Instance of ProjectConfig. @@ -161,32 +167,25 @@ def _send_impression_event(self, project_config, experiment, variation, user_id, attributes: Dict representing user attributes and values which need to be recorded. """ - user_event = user_event_factory.UserEventFactory.create_impression_event( - project_config, - experiment, - variation.id, - user_id, - attributes - ) - - self.event_processor.process(user_event) - - # Kept for backward compatibility. - # This notification is deprecated and new Decision notifications - # are sent via their respective method calls. - if len(self.notification_center.notification_listeners[enums.NotificationTypes.ACTIVATE]) > 0: - log_event = event_factory.EventFactory.create_log_event(user_event, self.logger) - self.notification_center.send_notifications(enums.NotificationTypes.ACTIVATE, experiment, - user_id, attributes, variation, log_event.__dict__) - - def _get_feature_variable_for_type(self, - project_config, - feature_key, - variable_key, - variable_type, - user_id, - attributes): - """ Helper method to determine value for a certain variable attached to a feature flag based on type of variable. + user_event = user_event_factory.UserEventFactory.create_impression_event( + project_config, experiment, variation.id, user_id, attributes + ) + + self.event_processor.process(user_event) + + # Kept for backward compatibility. + # This notification is deprecated and new Decision notifications + # are sent via their respective method calls. + if len(self.notification_center.notification_listeners[enums.NotificationTypes.ACTIVATE]) > 0: + log_event = event_factory.EventFactory.create_log_event(user_event, self.logger) + self.notification_center.send_notifications( + enums.NotificationTypes.ACTIVATE, experiment, user_id, attributes, variation, log_event.__dict__, + ) + + def _get_feature_variable_for_type( + self, project_config, feature_key, variable_key, variable_type, user_id, attributes, + ): + """ 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. @@ -202,94 +201,93 @@ def _get_feature_variable_for_type(self, - 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 - - if not validator.is_non_empty_string(variable_key): - self.logger.error(enums.Errors.INVALID_INPUT.format('variable_key')) - return None - - if not isinstance(user_id, string_types): - self.logger.error(enums.Errors.INVALID_INPUT.format('user_id')) - return None - - if not self._validate_user_inputs(attributes): - return None - - feature_flag = project_config.get_feature_from_key(feature_key) - if not feature_flag: - return None - - variable = project_config.get_variable_for_feature(feature_key, variable_key) - if not variable: - return None - - # For non-typed method, use type of variable; else, return None if type differs - variable_type = variable_type or variable.type - if variable.type != variable_type: - self.logger.warning( - 'Requested variable type "%s", but variable is of type "%s". ' - 'Use correct API to retrieve value. Returning None.' % (variable_type, variable.type) - ) - return None - - feature_enabled = False - source_info = {} - variable_value = variable.defaultValue - decision = self.decision_service.get_variation_for_feature(project_config, feature_flag, user_id, attributes) - if decision.variation: - - feature_enabled = decision.variation.featureEnabled - if feature_enabled: - variable_value = project_config.get_variable_value_for_variation(variable, decision.variation) - self.logger.info( - 'Got variable value "%s" for variable "%s" of feature flag "%s".' % ( - variable_value, variable_key, feature_key - ) - ) - else: - self.logger.info( - 'Feature "%s" for variation "%s" is not enabled. ' - 'Returning the default variable value "%s".' % (feature_key, decision.variation.key, variable_value) + if not validator.is_non_empty_string(feature_key): + self.logger.error(enums.Errors.INVALID_INPUT.format('feature_key')) + return None + + if not validator.is_non_empty_string(variable_key): + self.logger.error(enums.Errors.INVALID_INPUT.format('variable_key')) + return None + + if not isinstance(user_id, string_types): + self.logger.error(enums.Errors.INVALID_INPUT.format('user_id')) + return None + + if not self._validate_user_inputs(attributes): + return None + + feature_flag = project_config.get_feature_from_key(feature_key) + if not feature_flag: + return None + + variable = project_config.get_variable_for_feature(feature_key, variable_key) + if not variable: + return None + + # For non-typed method, use type of variable; else, return None if type differs + variable_type = variable_type or variable.type + if variable.type != variable_type: + self.logger.warning( + 'Requested variable type "%s", but variable is of type "%s". ' + 'Use correct API to retrieve value. Returning None.' % (variable_type, variable.type) + ) + return None + + feature_enabled = False + source_info = {} + variable_value = variable.defaultValue + decision = self.decision_service.get_variation_for_feature(project_config, feature_flag, user_id, attributes) + if decision.variation: + + feature_enabled = decision.variation.featureEnabled + if feature_enabled: + variable_value = project_config.get_variable_value_for_variation(variable, decision.variation) + self.logger.info( + 'Got variable value "%s" for variable "%s" of feature flag "%s".' + % (variable_value, variable_key, feature_key) + ) + else: + self.logger.info( + 'Feature "%s" for variation "%s" is not enabled. ' + 'Returning the default variable value "%s".' % (feature_key, decision.variation.key, variable_value) + ) + else: + self.logger.info( + 'User "%s" is not in any variation or rollout rule. ' + 'Returning default value for variable "%s" of feature flag "%s".' % (user_id, variable_key, feature_key) + ) + + if decision.source == enums.DecisionSources.FEATURE_TEST: + source_info = { + 'experiment_key': decision.experiment.key, + 'variation_key': decision.variation.key, + } + + try: + actual_value = project_config.get_typecast_value(variable_value, variable_type) + except: + self.logger.error('Unable to cast value. Returning None.') + actual_value = None + + self.notification_center.send_notifications( + enums.NotificationTypes.DECISION, + enums.DecisionNotificationTypes.FEATURE_VARIABLE, + user_id, + attributes or {}, + { + 'feature_key': feature_key, + 'feature_enabled': feature_enabled, + 'source': decision.source, + 'variable_key': variable_key, + 'variable_value': actual_value, + 'variable_type': variable_type, + 'source_info': source_info, + }, ) - else: - self.logger.info( - 'User "%s" is not in any variation or rollout rule. ' - 'Returning default value for variable "%s" of feature flag "%s".' % (user_id, variable_key, feature_key) - ) - - if decision.source == enums.DecisionSources.FEATURE_TEST: - source_info = { - 'experiment_key': decision.experiment.key, - 'variation_key': decision.variation.key - } - - try: - actual_value = project_config.get_typecast_value(variable_value, variable_type) - except: - self.logger.error('Unable to cast value. Returning None.') - actual_value = None - - self.notification_center.send_notifications( - enums.NotificationTypes.DECISION, - enums.DecisionNotificationTypes.FEATURE_VARIABLE, - user_id, - attributes or {}, - { - 'feature_key': feature_key, - 'feature_enabled': feature_enabled, - 'source': decision.source, - 'variable_key': variable_key, - 'variable_value': actual_value, - 'variable_type': variable_type, - 'source_info': source_info - } - ) - return actual_value - - def activate(self, experiment_key, user_id, attributes=None): - """ Buckets visitor and sends impression event to Optimizely. + return actual_value + + 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. @@ -301,40 +299,40 @@ def activate(self, experiment_key, user_id, attributes=None): 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')) - return None + if not self.is_valid: + self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('activate')) + return None - if not validator.is_non_empty_string(experiment_key): - self.logger.error(enums.Errors.INVALID_INPUT.format('experiment_key')) - return None + if not validator.is_non_empty_string(experiment_key): + self.logger.error(enums.Errors.INVALID_INPUT.format('experiment_key')) + return None - if not isinstance(user_id, string_types): - self.logger.error(enums.Errors.INVALID_INPUT.format('user_id')) - return None + if not isinstance(user_id, string_types): + self.logger.error(enums.Errors.INVALID_INPUT.format('user_id')) + return None - project_config = self.config_manager.get_config() - if not project_config: - self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('activate')) - return None + project_config = self.config_manager.get_config() + if not project_config: + self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('activate')) + return None - variation_key = self.get_variation(experiment_key, user_id, attributes) + variation_key = self.get_variation(experiment_key, user_id, attributes) - if not variation_key: - self.logger.info('Not activating user "%s".' % user_id) - return None + if not variation_key: + self.logger.info('Not activating user "%s".' % user_id) + return None - experiment = project_config.get_experiment_from_key(experiment_key) - variation = project_config.get_variation_from_key(experiment_key, variation_key) + experiment = project_config.get_experiment_from_key(experiment_key) + variation = project_config.get_variation_from_key(experiment_key, variation_key) - # Create and dispatch impression event - self.logger.info('Activating user "%s" in experiment "%s".' % (user_id, experiment.key)) - self._send_impression_event(project_config, experiment, variation, user_id, attributes) + # Create and dispatch impression event + self.logger.info('Activating user "%s" in experiment "%s".' % (user_id, experiment.key)) + self._send_impression_event(project_config, experiment, variation, user_id, attributes) - return variation.key + return variation.key - def track(self, event_key, user_id, attributes=None, event_tags=None): - """ Send conversion event to Optimizely. + 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. @@ -343,49 +341,46 @@ def track(self, event_key, user_id, attributes=None, event_tags=None): event_tags: Dict representing metadata associated with the event. """ - if not self.is_valid: - self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('track')) - return + if not self.is_valid: + self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('track')) + return - if not validator.is_non_empty_string(event_key): - self.logger.error(enums.Errors.INVALID_INPUT.format('event_key')) - return + if not validator.is_non_empty_string(event_key): + self.logger.error(enums.Errors.INVALID_INPUT.format('event_key')) + return - if not isinstance(user_id, string_types): - self.logger.error(enums.Errors.INVALID_INPUT.format('user_id')) - return + if not isinstance(user_id, string_types): + self.logger.error(enums.Errors.INVALID_INPUT.format('user_id')) + return - if not self._validate_user_inputs(attributes, event_tags): - return + if not self._validate_user_inputs(attributes, event_tags): + return - project_config = self.config_manager.get_config() - if not project_config: - self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('track')) - return + project_config = self.config_manager.get_config() + if not project_config: + self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('track')) + return - event = project_config.get_event(event_key) - if not event: - self.logger.info('Not tracking user "%s" for event "%s".' % (user_id, event_key)) - return + event = project_config.get_event(event_key) + if not event: + self.logger.info('Not tracking user "%s" for event "%s".' % (user_id, event_key)) + return - user_event = user_event_factory.UserEventFactory.create_conversion_event( - project_config, - event_key, - user_id, - attributes, - event_tags - ) + user_event = user_event_factory.UserEventFactory.create_conversion_event( + project_config, event_key, user_id, attributes, event_tags + ) - self.event_processor.process(user_event) - self.logger.info('Tracking event "%s" for user "%s".' % (event_key, user_id)) + self.event_processor.process(user_event) + self.logger.info('Tracking event "%s" for user "%s".' % (event_key, user_id)) - if len(self.notification_center.notification_listeners[enums.NotificationTypes.TRACK]) > 0: - log_event = event_factory.EventFactory.create_log_event(user_event, self.logger) - self.notification_center.send_notifications(enums.NotificationTypes.TRACK, event_key, user_id, - attributes, event_tags, log_event.__dict__) + if len(self.notification_center.notification_listeners[enums.NotificationTypes.TRACK]) > 0: + log_event = event_factory.EventFactory.create_log_event(user_event, self.logger) + self.notification_center.send_notifications( + enums.NotificationTypes.TRACK, event_key, user_id, attributes, event_tags, log_event.__dict__, + ) - def get_variation(self, experiment_key, user_id, attributes=None): - """ Gets variation where user will be bucketed. + 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. @@ -397,60 +392,54 @@ def get_variation(self, experiment_key, user_id, attributes=None): 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')) - return None - - if not validator.is_non_empty_string(experiment_key): - self.logger.error(enums.Errors.INVALID_INPUT.format('experiment_key')) - return None - - if not isinstance(user_id, string_types): - self.logger.error(enums.Errors.INVALID_INPUT.format('user_id')) - return None - - project_config = self.config_manager.get_config() - if not project_config: - self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_variation')) - return None - - experiment = project_config.get_experiment_from_key(experiment_key) - variation_key = None - - if not experiment: - self.logger.info('Experiment key "%s" is invalid. Not activating user "%s".' % ( - experiment_key, - user_id - )) - return None - - if not self._validate_user_inputs(attributes): - return None - - variation = self.decision_service.get_variation(project_config, experiment, user_id, attributes) - if variation: - variation_key = variation.key - - if project_config.is_feature_experiment(experiment.id): - decision_notification_type = enums.DecisionNotificationTypes.FEATURE_TEST - else: - decision_notification_type = enums.DecisionNotificationTypes.AB_TEST - - self.notification_center.send_notifications( - enums.NotificationTypes.DECISION, - decision_notification_type, - user_id, - attributes or {}, - { - 'experiment_key': experiment_key, - 'variation_key': variation_key - } - ) - - return variation_key - - def is_feature_enabled(self, feature_key, user_id, attributes=None): - """ Returns true if the feature is enabled for the given user. + if not self.is_valid: + self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('get_variation')) + return None + + if not validator.is_non_empty_string(experiment_key): + self.logger.error(enums.Errors.INVALID_INPUT.format('experiment_key')) + return None + + if not isinstance(user_id, string_types): + self.logger.error(enums.Errors.INVALID_INPUT.format('user_id')) + return None + + project_config = self.config_manager.get_config() + if not project_config: + self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_variation')) + return None + + experiment = project_config.get_experiment_from_key(experiment_key) + variation_key = None + + if not experiment: + self.logger.info('Experiment key "%s" is invalid. Not activating user "%s".' % (experiment_key, user_id)) + return None + + if not self._validate_user_inputs(attributes): + return None + + variation = self.decision_service.get_variation(project_config, experiment, user_id, attributes) + if variation: + variation_key = variation.key + + if project_config.is_feature_experiment(experiment.id): + decision_notification_type = enums.DecisionNotificationTypes.FEATURE_TEST + else: + decision_notification_type = enums.DecisionNotificationTypes.AB_TEST + + self.notification_center.send_notifications( + enums.NotificationTypes.DECISION, + decision_notification_type, + user_id, + attributes or {}, + {'experiment_key': experiment_key, 'variation_key': variation_key}, + ) + + return variation_key + + 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. @@ -461,72 +450,70 @@ def is_feature_enabled(self, feature_key, user_id, attributes=None): 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')) - return False - - if not validator.is_non_empty_string(feature_key): - self.logger.error(enums.Errors.INVALID_INPUT.format('feature_key')) - return False - - if not isinstance(user_id, string_types): - self.logger.error(enums.Errors.INVALID_INPUT.format('user_id')) - return False - - if not self._validate_user_inputs(attributes): - return False - - project_config = self.config_manager.get_config() - if not project_config: - self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('is_feature_enabled')) - return False - - feature = project_config.get_feature_from_key(feature_key) - if not feature: - return False - - feature_enabled = False - source_info = {} - decision = self.decision_service.get_variation_for_feature(project_config, feature, user_id, attributes) - is_source_experiment = decision.source == enums.DecisionSources.FEATURE_TEST - - if decision.variation: - if decision.variation.featureEnabled is True: - feature_enabled = True - # Send event if Decision came from an experiment. - if is_source_experiment: - source_info = { - 'experiment_key': decision.experiment.key, - 'variation_key': decision.variation.key - } - self._send_impression_event(project_config, - decision.experiment, - decision.variation, - user_id, - attributes) - - if feature_enabled: - self.logger.info('Feature "%s" is enabled for user "%s".' % (feature_key, user_id)) - else: - self.logger.info('Feature "%s" is not enabled for user "%s".' % (feature_key, user_id)) - - self.notification_center.send_notifications( - enums.NotificationTypes.DECISION, - enums.DecisionNotificationTypes.FEATURE, - user_id, - attributes or {}, - { - 'feature_key': feature_key, - 'feature_enabled': feature_enabled, - 'source': decision.source, - 'source_info': source_info - } - ) - - return feature_enabled - - def get_enabled_features(self, user_id, attributes=None): - """ Returns the list of features that are enabled for the user. + if not self.is_valid: + self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('is_feature_enabled')) + return False + + if not validator.is_non_empty_string(feature_key): + self.logger.error(enums.Errors.INVALID_INPUT.format('feature_key')) + return False + + if not isinstance(user_id, string_types): + self.logger.error(enums.Errors.INVALID_INPUT.format('user_id')) + return False + + if not self._validate_user_inputs(attributes): + return False + + project_config = self.config_manager.get_config() + if not project_config: + self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('is_feature_enabled')) + return False + + feature = project_config.get_feature_from_key(feature_key) + if not feature: + return False + + feature_enabled = False + source_info = {} + decision = self.decision_service.get_variation_for_feature(project_config, feature, user_id, attributes) + is_source_experiment = decision.source == enums.DecisionSources.FEATURE_TEST + + if decision.variation: + if decision.variation.featureEnabled is True: + feature_enabled = True + # Send event if Decision came from an experiment. + if is_source_experiment: + source_info = { + 'experiment_key': decision.experiment.key, + 'variation_key': decision.variation.key, + } + self._send_impression_event( + project_config, decision.experiment, decision.variation, user_id, attributes, + ) + + if feature_enabled: + self.logger.info('Feature "%s" is enabled for user "%s".' % (feature_key, user_id)) + else: + self.logger.info('Feature "%s" is not enabled for user "%s".' % (feature_key, user_id)) + + self.notification_center.send_notifications( + enums.NotificationTypes.DECISION, + enums.DecisionNotificationTypes.FEATURE, + user_id, + attributes or {}, + { + 'feature_key': feature_key, + 'feature_enabled': feature_enabled, + 'source': decision.source, + 'source_info': source_info, + }, + ) + + return feature_enabled + + 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. @@ -536,31 +523,31 @@ def get_enabled_features(self, user_id, attributes=None): A list of the keys of the features that are enabled for the user. """ - enabled_features = [] - if not self.is_valid: - self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('get_enabled_features')) - return enabled_features + enabled_features = [] + if not self.is_valid: + self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('get_enabled_features')) + return enabled_features - if not isinstance(user_id, string_types): - self.logger.error(enums.Errors.INVALID_INPUT.format('user_id')) - return enabled_features + if not isinstance(user_id, string_types): + self.logger.error(enums.Errors.INVALID_INPUT.format('user_id')) + return enabled_features - if not self._validate_user_inputs(attributes): - return enabled_features + if not self._validate_user_inputs(attributes): + return enabled_features - project_config = self.config_manager.get_config() - if not project_config: - self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_enabled_features')) - return enabled_features + project_config = self.config_manager.get_config() + if not project_config: + self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_enabled_features')) + return enabled_features - for feature in project_config.feature_key_map.values(): - if self.is_feature_enabled(feature.key, user_id, attributes): - enabled_features.append(feature.key) + for feature in project_config.feature_key_map.values(): + if self.is_feature_enabled(feature.key, user_id, attributes): + enabled_features.append(feature.key) - return enabled_features + return enabled_features - def get_feature_variable(self, feature_key, variable_key, user_id, attributes=None): - """ Returns value for a variable attached to a feature flag. + 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. @@ -573,15 +560,15 @@ def get_feature_variable(self, feature_key, variable_key, user_id, attributes=No - 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')) - return None + project_config = self.config_manager.get_config() + if not project_config: + self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_feature_variable')) + return None - return self._get_feature_variable_for_type(project_config, feature_key, variable_key, None, user_id, attributes) + return self._get_feature_variable_for_type(project_config, feature_key, variable_key, None, user_id, attributes) - 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. + 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. @@ -596,18 +583,18 @@ def get_feature_variable_boolean(self, feature_key, variable_key, user_id, attri - Mismatch with type of variable. """ - variable_type = entities.Variable.Type.BOOLEAN - project_config = self.config_manager.get_config() - if not project_config: - self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_feature_variable_boolean')) - return None + variable_type = entities.Variable.Type.BOOLEAN + project_config = self.config_manager.get_config() + if not project_config: + self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_feature_variable_boolean')) + return None - return self._get_feature_variable_for_type( - project_config, feature_key, variable_key, variable_type, user_id, attributes - ) + return self._get_feature_variable_for_type( + project_config, feature_key, variable_key, variable_type, user_id, attributes, + ) - 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. + 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. @@ -622,18 +609,18 @@ def get_feature_variable_double(self, feature_key, variable_key, user_id, attrib - Mismatch with type of variable. """ - variable_type = entities.Variable.Type.DOUBLE - project_config = self.config_manager.get_config() - if not project_config: - self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_feature_variable_double')) - return None + variable_type = entities.Variable.Type.DOUBLE + project_config = self.config_manager.get_config() + if not project_config: + self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_feature_variable_double')) + return None - return self._get_feature_variable_for_type( - project_config, feature_key, variable_key, variable_type, user_id, attributes - ) + return self._get_feature_variable_for_type( + project_config, feature_key, variable_key, variable_type, user_id, attributes, + ) - 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. + 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. @@ -648,18 +635,18 @@ def get_feature_variable_integer(self, feature_key, variable_key, user_id, attri - Mismatch with type of variable. """ - variable_type = entities.Variable.Type.INTEGER - project_config = self.config_manager.get_config() - if not project_config: - self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_feature_variable_integer')) - return None + variable_type = entities.Variable.Type.INTEGER + project_config = self.config_manager.get_config() + if not project_config: + self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_feature_variable_integer')) + return None - return self._get_feature_variable_for_type( - project_config, feature_key, variable_key, variable_type, user_id, attributes - ) + return self._get_feature_variable_for_type( + project_config, feature_key, variable_key, variable_type, user_id, attributes, + ) - 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. + 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. @@ -674,18 +661,18 @@ def get_feature_variable_string(self, feature_key, variable_key, user_id, attrib - Mismatch with type of variable. """ - variable_type = entities.Variable.Type.STRING - project_config = self.config_manager.get_config() - if not project_config: - self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_feature_variable_string')) - return None + variable_type = entities.Variable.Type.STRING + project_config = self.config_manager.get_config() + if not project_config: + self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_feature_variable_string')) + return None - return self._get_feature_variable_for_type( - project_config, feature_key, variable_key, variable_type, user_id, attributes - ) + return self._get_feature_variable_for_type( + project_config, feature_key, variable_key, variable_type, user_id, attributes, + ) - def set_forced_variation(self, experiment_key, user_id, variation_key): - """ Force a user into a variation for a given experiment. + 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. @@ -697,27 +684,27 @@ def set_forced_variation(self, experiment_key, user_id, variation_key): 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')) - return False + if not self.is_valid: + self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('set_forced_variation')) + return False - if not validator.is_non_empty_string(experiment_key): - self.logger.error(enums.Errors.INVALID_INPUT.format('experiment_key')) - return False + if not validator.is_non_empty_string(experiment_key): + self.logger.error(enums.Errors.INVALID_INPUT.format('experiment_key')) + return False - if not isinstance(user_id, string_types): - self.logger.error(enums.Errors.INVALID_INPUT.format('user_id')) - return False + if not isinstance(user_id, string_types): + self.logger.error(enums.Errors.INVALID_INPUT.format('user_id')) + return False - project_config = self.config_manager.get_config() - if not project_config: - self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('set_forced_variation')) - return False + project_config = self.config_manager.get_config() + if not project_config: + self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('set_forced_variation')) + return False - return self.decision_service.set_forced_variation(project_config, experiment_key, user_id, variation_key) + return self.decision_service.set_forced_variation(project_config, 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. + 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. @@ -727,22 +714,22 @@ def get_forced_variation(self, experiment_key, user_id): 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')) - return None + if not self.is_valid: + self.logger.error(enums.Errors.INVALID_OPTIMIZELY.format('get_forced_variation')) + return None - if not validator.is_non_empty_string(experiment_key): - self.logger.error(enums.Errors.INVALID_INPUT.format('experiment_key')) - return None + if not validator.is_non_empty_string(experiment_key): + self.logger.error(enums.Errors.INVALID_INPUT.format('experiment_key')) + return None - if not isinstance(user_id, string_types): - self.logger.error(enums.Errors.INVALID_INPUT.format('user_id')) - return None + if not isinstance(user_id, string_types): + self.logger.error(enums.Errors.INVALID_INPUT.format('user_id')) + return None - project_config = self.config_manager.get_config() - if not project_config: - self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_forced_variation')) - return None + project_config = self.config_manager.get_config() + if not project_config: + self.logger.error(enums.Errors.INVALID_PROJECT_CONFIG.format('get_forced_variation')) + return None - forced_variation = self.decision_service.get_forced_variation(project_config, experiment_key, user_id) - return forced_variation.key if forced_variation else None + forced_variation = self.decision_service.get_forced_variation(project_config, experiment_key, user_id) + return forced_variation.key if forced_variation else None diff --git a/optimizely/project_config.py b/optimizely/project_config.py index 52e58837..b944015e 100644 --- a/optimizely/project_config.py +++ b/optimizely/project_config.py @@ -18,16 +18,20 @@ from . import entities from . import exceptions -SUPPORTED_VERSIONS = [enums.DatafileVersions.V2, enums.DatafileVersions.V3, enums.DatafileVersions.V4] +SUPPORTED_VERSIONS = [ + enums.DatafileVersions.V2, + enums.DatafileVersions.V3, + enums.DatafileVersions.V4, +] RESERVED_ATTRIBUTE_PREFIX = '$opt_' class ProjectConfig(object): - """ Representation of the Optimizely project config. """ + """ Representation of the Optimizely project config. """ - def __init__(self, datafile, logger, error_handler): - """ ProjectConfig init method to load and set project config data. + def __init__(self, datafile, logger, error_handler): + """ ProjectConfig init method to load and set project config data. Args: datafile: JSON string representing the project. @@ -35,97 +39,94 @@ def __init__(self, datafile, logger, error_handler): error_handler: Provides a handle_error method to handle exceptions. """ - config = json.loads(datafile) - self.logger = logger - self.error_handler = error_handler - self.version = config.get('version') - if self.version not in SUPPORTED_VERSIONS: - raise exceptions.UnsupportedDatafileVersionException( - enums.Errors.UNSUPPORTED_DATAFILE_VERSION.format(self.version) - ) - - self.account_id = config.get('accountId') - self.project_id = config.get('projectId') - self.revision = config.get('revision') - self.groups = config.get('groups', []) - self.experiments = config.get('experiments', []) - self.events = config.get('events', []) - self.attributes = config.get('attributes', []) - self.audiences = config.get('audiences', []) - self.typed_audiences = config.get('typedAudiences', []) - self.feature_flags = config.get('featureFlags', []) - self.rollouts = config.get('rollouts', []) - self.anonymize_ip = config.get('anonymizeIP', False) - self.bot_filtering = config.get('botFiltering', None) - - # Utility maps for quick lookup - self.group_id_map = self._generate_key_map(self.groups, 'id', entities.Group) - self.experiment_key_map = self._generate_key_map(self.experiments, 'key', entities.Experiment) - self.event_key_map = self._generate_key_map(self.events, 'key', entities.Event) - self.attribute_key_map = self._generate_key_map(self.attributes, 'key', entities.Attribute) - - self.audience_id_map = self._generate_key_map(self.audiences, 'id', entities.Audience) - - # Conditions of audiences in typedAudiences are not expected - # to be string-encoded as they are in audiences. - for typed_audience in self.typed_audiences: - typed_audience['conditions'] = json.dumps(typed_audience['conditions']) - typed_audience_id_map = self._generate_key_map(self.typed_audiences, 'id', entities.Audience) - self.audience_id_map.update(typed_audience_id_map) - - self.rollout_id_map = self._generate_key_map(self.rollouts, 'id', entities.Layer) - for layer in self.rollout_id_map.values(): - for experiment in layer.experiments: - self.experiment_key_map[experiment['key']] = entities.Experiment(**experiment) - - self.audience_id_map = self._deserialize_audience(self.audience_id_map) - for group in self.group_id_map.values(): - experiments_in_group_key_map = self._generate_key_map(group.experiments, 'key', entities.Experiment) - for experiment in experiments_in_group_key_map.values(): - experiment.__dict__.update({ - 'groupId': group.id, - 'groupPolicy': group.policy - }) - self.experiment_key_map.update(experiments_in_group_key_map) - - self.experiment_id_map = {} - self.variation_key_map = {} - self.variation_id_map = {} - self.variation_variable_usage_map = {} - for experiment in self.experiment_key_map.values(): - self.experiment_id_map[experiment.id] = experiment - self.variation_key_map[experiment.key] = self._generate_key_map( - experiment.variations, 'key', entities.Variation - ) - self.variation_id_map[experiment.key] = {} - for variation in self.variation_key_map.get(experiment.key).values(): - self.variation_id_map[experiment.key][variation.id] = variation - self.variation_variable_usage_map[variation.id] = self._generate_key_map( - variation.variables, 'id', entities.Variation.VariableUsage - ) - - self.feature_key_map = self._generate_key_map(self.feature_flags, 'key', entities.FeatureFlag) - - # 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: - # Add this experiment in experiment-feature map. - self.experiment_feature_map[exp_id] = [feature.id] - - experiment_in_feature = self.experiment_id_map[exp_id] - # Check if any of the experiments are in a group and add the group id for faster bucketing later on - if experiment_in_feature.groupId: - feature.groupId = experiment_in_feature.groupId - # Experiments in feature can only belong to one mutex group - break - - @staticmethod - def _generate_key_map(entity_list, key, entity_class): - """ Helper method to generate map from key to entity object for given list of dicts. + config = json.loads(datafile) + self.logger = logger + self.error_handler = error_handler + self.version = config.get('version') + if self.version not in SUPPORTED_VERSIONS: + raise exceptions.UnsupportedDatafileVersionException( + enums.Errors.UNSUPPORTED_DATAFILE_VERSION.format(self.version) + ) + + self.account_id = config.get('accountId') + self.project_id = config.get('projectId') + self.revision = config.get('revision') + self.groups = config.get('groups', []) + self.experiments = config.get('experiments', []) + self.events = config.get('events', []) + self.attributes = config.get('attributes', []) + self.audiences = config.get('audiences', []) + self.typed_audiences = config.get('typedAudiences', []) + self.feature_flags = config.get('featureFlags', []) + self.rollouts = config.get('rollouts', []) + self.anonymize_ip = config.get('anonymizeIP', False) + self.bot_filtering = config.get('botFiltering', None) + + # Utility maps for quick lookup + self.group_id_map = self._generate_key_map(self.groups, 'id', entities.Group) + self.experiment_key_map = self._generate_key_map(self.experiments, 'key', entities.Experiment) + self.event_key_map = self._generate_key_map(self.events, 'key', entities.Event) + self.attribute_key_map = self._generate_key_map(self.attributes, 'key', entities.Attribute) + + self.audience_id_map = self._generate_key_map(self.audiences, 'id', entities.Audience) + + # Conditions of audiences in typedAudiences are not expected + # to be string-encoded as they are in audiences. + for typed_audience in self.typed_audiences: + typed_audience['conditions'] = json.dumps(typed_audience['conditions']) + typed_audience_id_map = self._generate_key_map(self.typed_audiences, 'id', entities.Audience) + self.audience_id_map.update(typed_audience_id_map) + + self.rollout_id_map = self._generate_key_map(self.rollouts, 'id', entities.Layer) + for layer in self.rollout_id_map.values(): + for experiment in layer.experiments: + self.experiment_key_map[experiment['key']] = entities.Experiment(**experiment) + + self.audience_id_map = self._deserialize_audience(self.audience_id_map) + for group in self.group_id_map.values(): + experiments_in_group_key_map = self._generate_key_map(group.experiments, 'key', entities.Experiment) + for experiment in experiments_in_group_key_map.values(): + experiment.__dict__.update({'groupId': group.id, 'groupPolicy': group.policy}) + self.experiment_key_map.update(experiments_in_group_key_map) + + self.experiment_id_map = {} + self.variation_key_map = {} + self.variation_id_map = {} + self.variation_variable_usage_map = {} + for experiment in self.experiment_key_map.values(): + self.experiment_id_map[experiment.id] = experiment + self.variation_key_map[experiment.key] = self._generate_key_map( + experiment.variations, 'key', entities.Variation + ) + self.variation_id_map[experiment.key] = {} + for variation in self.variation_key_map.get(experiment.key).values(): + self.variation_id_map[experiment.key][variation.id] = variation + self.variation_variable_usage_map[variation.id] = self._generate_key_map( + variation.variables, 'id', entities.Variation.VariableUsage + ) + + self.feature_key_map = self._generate_key_map(self.feature_flags, 'key', entities.FeatureFlag) + + # 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: + # Add this experiment in experiment-feature map. + self.experiment_feature_map[exp_id] = [feature.id] + + experiment_in_feature = self.experiment_id_map[exp_id] + # Check if any of the experiments are in a group and add the group id for faster bucketing later on + if experiment_in_feature.groupId: + feature.groupId = experiment_in_feature.groupId + # Experiments in feature can only belong to one mutex group + break + + @staticmethod + def _generate_key_map(entity_list, key, entity_class): + """ Helper method to generate map from key to entity object for given list of dicts. Args: entity_list: List consisting of dict. @@ -136,15 +137,15 @@ def _generate_key_map(entity_list, key, entity_class): Map mapping key to entity object. """ - key_map = {} - for obj in entity_list: - key_map[obj[key]] = entity_class(**obj) + key_map = {} + for obj in entity_list: + key_map[obj[key]] = entity_class(**obj) - return key_map + return key_map - @staticmethod - def _deserialize_audience(audience_map): - """ Helper method to de-serialize and populate audience map with the condition list and structure. + @staticmethod + def _deserialize_audience(audience_map): + """ Helper method to de-serialize and populate audience map with the condition list and structure. Args: audience_map: Dict mapping audience ID to audience object. @@ -153,17 +154,14 @@ def _deserialize_audience(audience_map): Dict additionally consisting of condition list and structure on every audience object. """ - for audience in audience_map.values(): - condition_structure, condition_list = condition_helper.loads(audience.conditions) - audience.__dict__.update({ - 'conditionStructure': condition_structure, - 'conditionList': condition_list - }) + for audience in audience_map.values(): + condition_structure, condition_list = condition_helper.loads(audience.conditions) + audience.__dict__.update({'conditionStructure': condition_structure, 'conditionList': condition_list}) - return audience_map + return audience_map - def get_typecast_value(self, value, type): - """ Helper method to determine actual value based on type of feature variable. + def get_typecast_value(self, value, type): + """ Helper method to determine actual value based on type of feature variable. Args: value: Value in string form as it was parsed from datafile. @@ -173,53 +171,53 @@ def get_typecast_value(self, value, type): Value type-casted based on type of feature variable. """ - if type == entities.Variable.Type.BOOLEAN: - return value == 'true' - elif type == entities.Variable.Type.INTEGER: - return int(value) - elif type == entities.Variable.Type.DOUBLE: - return float(value) - else: - return value + if type == entities.Variable.Type.BOOLEAN: + return value == 'true' + elif type == entities.Variable.Type.INTEGER: + return int(value) + elif type == entities.Variable.Type.DOUBLE: + return float(value) + else: + return value - def get_version(self): - """ Get version of the datafile. + def get_version(self): + """ Get version of the datafile. Returns: Version of the datafile. """ - return self.version + return self.version - def get_revision(self): - """ Get revision of the datafile. + def get_revision(self): + """ Get revision of the datafile. Returns: Revision of the datafile. """ - return self.revision + return self.revision - def get_account_id(self): - """ Get account ID from the config. + def get_account_id(self): + """ Get account ID from the config. Returns: Account ID information from the config. """ - return self.account_id + return self.account_id - def get_project_id(self): - """ Get project ID from the config. + def get_project_id(self): + """ Get project ID from the config. Returns: Project ID information from the config. """ - return self.project_id + return self.project_id - def get_experiment_from_key(self, experiment_key): - """ Get experiment for the provided experiment key. + def get_experiment_from_key(self, experiment_key): + """ Get experiment for the provided experiment key. Args: experiment_key: Experiment key for which experiment is to be determined. @@ -228,17 +226,17 @@ def get_experiment_from_key(self, experiment_key): Experiment corresponding to the provided experiment key. """ - experiment = self.experiment_key_map.get(experiment_key) + experiment = self.experiment_key_map.get(experiment_key) - if experiment: - return experiment + if experiment: + return experiment - self.logger.error('Experiment key "%s" is not in datafile.' % experiment_key) - self.error_handler.handle_error(exceptions.InvalidExperimentException(enums.Errors.INVALID_EXPERIMENT_KEY)) - return None + self.logger.error('Experiment key "%s" is not in datafile.' % experiment_key) + self.error_handler.handle_error(exceptions.InvalidExperimentException(enums.Errors.INVALID_EXPERIMENT_KEY)) + return None - def get_experiment_from_id(self, experiment_id): - """ Get experiment for the provided experiment ID. + def get_experiment_from_id(self, experiment_id): + """ Get experiment for the provided experiment ID. Args: experiment_id: Experiment ID for which experiment is to be determined. @@ -247,17 +245,17 @@ def get_experiment_from_id(self, experiment_id): Experiment corresponding to the provided experiment ID. """ - experiment = self.experiment_id_map.get(experiment_id) + experiment = self.experiment_id_map.get(experiment_id) - if experiment: - return experiment + if experiment: + return experiment - self.logger.error('Experiment ID "%s" is not in datafile.' % experiment_id) - self.error_handler.handle_error(exceptions.InvalidExperimentException(enums.Errors.INVALID_EXPERIMENT_KEY)) - return None + self.logger.error('Experiment ID "%s" is not in datafile.' % experiment_id) + self.error_handler.handle_error(exceptions.InvalidExperimentException(enums.Errors.INVALID_EXPERIMENT_KEY)) + return None - def get_group(self, group_id): - """ Get group for the provided group ID. + def get_group(self, group_id): + """ Get group for the provided group ID. Args: group_id: Group ID for which group is to be determined. @@ -266,17 +264,17 @@ def get_group(self, group_id): Group corresponding to the provided group ID. """ - group = self.group_id_map.get(group_id) + group = self.group_id_map.get(group_id) - if group: - return group + if group: + return group - self.logger.error('Group ID "%s" is not in datafile.' % group_id) - self.error_handler.handle_error(exceptions.InvalidGroupException(enums.Errors.INVALID_GROUP_ID)) - return None + self.logger.error('Group ID "%s" is not in datafile.' % group_id) + self.error_handler.handle_error(exceptions.InvalidGroupException(enums.Errors.INVALID_GROUP_ID)) + return None - def get_audience(self, audience_id): - """ Get audience object for the provided audience ID. + def get_audience(self, audience_id): + """ Get audience object for the provided audience ID. Args: audience_id: ID of the audience. @@ -285,15 +283,15 @@ def get_audience(self, audience_id): Dict representing the audience. """ - audience = self.audience_id_map.get(audience_id) - if audience: - return audience + audience = self.audience_id_map.get(audience_id) + if audience: + return audience - self.logger.error('Audience ID "%s" is not in datafile.' % audience_id) - self.error_handler.handle_error(exceptions.InvalidAudienceException((enums.Errors.INVALID_AUDIENCE))) + self.logger.error('Audience ID "%s" is not in datafile.' % audience_id) + self.error_handler.handle_error(exceptions.InvalidAudienceException((enums.Errors.INVALID_AUDIENCE))) - def get_variation_from_key(self, experiment_key, variation_key): - """ Get variation given experiment and variation key. + def get_variation_from_key(self, experiment_key, variation_key): + """ Get variation given experiment and variation key. Args: experiment: Key representing parent experiment of variation. @@ -303,23 +301,23 @@ def get_variation_from_key(self, experiment_key, variation_key): Object representing the variation. """ - variation_map = self.variation_key_map.get(experiment_key) + variation_map = self.variation_key_map.get(experiment_key) - if variation_map: - variation = variation_map.get(variation_key) - if variation: - return variation - else: - self.logger.error('Variation key "%s" is not in datafile.' % variation_key) - self.error_handler.handle_error(exceptions.InvalidVariationException(enums.Errors.INVALID_VARIATION)) - return None + if variation_map: + variation = variation_map.get(variation_key) + if variation: + return variation + else: + self.logger.error('Variation key "%s" is not in datafile.' % variation_key) + self.error_handler.handle_error(exceptions.InvalidVariationException(enums.Errors.INVALID_VARIATION)) + return None - self.logger.error('Experiment key "%s" is not in datafile.' % experiment_key) - self.error_handler.handle_error(exceptions.InvalidExperimentException(enums.Errors.INVALID_EXPERIMENT_KEY)) - return None + self.logger.error('Experiment key "%s" is not in datafile.' % experiment_key) + self.error_handler.handle_error(exceptions.InvalidExperimentException(enums.Errors.INVALID_EXPERIMENT_KEY)) + return None - def get_variation_from_id(self, experiment_key, variation_id): - """ Get variation given experiment and variation ID. + def get_variation_from_id(self, experiment_key, variation_id): + """ Get variation given experiment and variation ID. Args: experiment: Key representing parent experiment of variation. @@ -329,23 +327,23 @@ def get_variation_from_id(self, experiment_key, variation_id): Object representing the variation. """ - variation_map = self.variation_id_map.get(experiment_key) + variation_map = self.variation_id_map.get(experiment_key) - if variation_map: - variation = variation_map.get(variation_id) - if variation: - return variation - else: - self.logger.error('Variation ID "%s" is not in datafile.' % variation_id) - self.error_handler.handle_error(exceptions.InvalidVariationException(enums.Errors.INVALID_VARIATION)) - return None + if variation_map: + variation = variation_map.get(variation_id) + if variation: + return variation + else: + self.logger.error('Variation ID "%s" is not in datafile.' % variation_id) + self.error_handler.handle_error(exceptions.InvalidVariationException(enums.Errors.INVALID_VARIATION)) + return None - self.logger.error('Experiment key "%s" is not in datafile.' % experiment_key) - self.error_handler.handle_error(exceptions.InvalidExperimentException(enums.Errors.INVALID_EXPERIMENT_KEY)) - return None + self.logger.error('Experiment key "%s" is not in datafile.' % experiment_key) + self.error_handler.handle_error(exceptions.InvalidExperimentException(enums.Errors.INVALID_EXPERIMENT_KEY)) + return None - def get_event(self, event_key): - """ Get event for the provided event key. + def get_event(self, event_key): + """ Get event for the provided event key. Args: event_key: Event key for which event is to be determined. @@ -354,17 +352,17 @@ def get_event(self, event_key): Event corresponding to the provided event key. """ - event = self.event_key_map.get(event_key) + event = self.event_key_map.get(event_key) - if event: - return event + if event: + return event - self.logger.error('Event "%s" is not in datafile.' % event_key) - self.error_handler.handle_error(exceptions.InvalidEventException(enums.Errors.INVALID_EVENT_KEY)) - return None + self.logger.error('Event "%s" is not in datafile.' % event_key) + self.error_handler.handle_error(exceptions.InvalidEventException(enums.Errors.INVALID_EVENT_KEY)) + return None - def get_attribute_id(self, attribute_key): - """ Get attribute ID for the provided attribute key. + def get_attribute_id(self, attribute_key): + """ Get attribute ID for the provided attribute key. Args: attribute_key: Attribute key for which attribute is to be fetched. @@ -373,25 +371,29 @@ def get_attribute_id(self, attribute_key): Attribute ID corresponding to the provided attribute key. """ - attribute = self.attribute_key_map.get(attribute_key) - has_reserved_prefix = attribute_key.startswith(RESERVED_ATTRIBUTE_PREFIX) + attribute = self.attribute_key_map.get(attribute_key) + has_reserved_prefix = attribute_key.startswith(RESERVED_ATTRIBUTE_PREFIX) - if attribute: - if has_reserved_prefix: - self.logger.warning(('Attribute %s unexpectedly has reserved prefix %s; using attribute ID ' - 'instead of reserved attribute name.' % (attribute_key, RESERVED_ATTRIBUTE_PREFIX))) + if attribute: + if has_reserved_prefix: + self.logger.warning( + ( + 'Attribute %s unexpectedly has reserved prefix %s; using attribute ID ' + 'instead of reserved attribute name.' % (attribute_key, RESERVED_ATTRIBUTE_PREFIX) + ) + ) - return attribute.id + return attribute.id - if has_reserved_prefix: - return attribute_key + if has_reserved_prefix: + return attribute_key - self.logger.error('Attribute "%s" is not in datafile.' % attribute_key) - self.error_handler.handle_error(exceptions.InvalidAttributeException(enums.Errors.INVALID_ATTRIBUTE)) - return None + self.logger.error('Attribute "%s" is not in datafile.' % attribute_key) + self.error_handler.handle_error(exceptions.InvalidAttributeException(enums.Errors.INVALID_ATTRIBUTE)) + return None - def get_feature_from_key(self, feature_key): - """ Get feature for the provided feature key. + def get_feature_from_key(self, feature_key): + """ Get feature for the provided feature key. Args: feature_key: Feature key for which feature is to be fetched. @@ -399,16 +401,16 @@ def get_feature_from_key(self, feature_key): Returns: Feature corresponding to the provided feature key. """ - feature = self.feature_key_map.get(feature_key) + feature = self.feature_key_map.get(feature_key) - if feature: - return feature + if feature: + return feature - self.logger.error('Feature "%s" is not in datafile.' % feature_key) - return None + self.logger.error('Feature "%s" is not in datafile.' % feature_key) + return None - def get_rollout_from_id(self, rollout_id): - """ Get rollout for the provided ID. + def get_rollout_from_id(self, rollout_id): + """ Get rollout for the provided ID. Args: rollout_id: ID of the rollout to be fetched. @@ -416,16 +418,16 @@ def get_rollout_from_id(self, rollout_id): Returns: Rollout corresponding to the provided ID. """ - layer = self.rollout_id_map.get(rollout_id) + layer = self.rollout_id_map.get(rollout_id) - if layer: - return layer + if layer: + return layer - self.logger.error('Rollout with ID "%s" is not in datafile.' % rollout_id) - return None + self.logger.error('Rollout with ID "%s" is not in datafile.' % rollout_id) + return None - def get_variable_value_for_variation(self, variable, variation): - """ Get the variable value for the given variation. + def get_variable_value_for_variation(self, variable, variation): + """ Get the variable value for the given variation. Args: variable: The Variable for which we are getting the value. @@ -435,41 +437,38 @@ def get_variable_value_for_variation(self, variable, variation): The variable value or None if any of the inputs are invalid. """ - if not variable or not variation: - return None + 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 + 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 - # Get all variable usages for the given variation - variable_usages = self.variation_variable_usage_map[variation.id] + # Get all variable usages for the given variation + variable_usages = self.variation_variable_usage_map[variation.id] - # Find usage in given variation - variable_usage = None - if variable_usages: - variable_usage = variable_usages.get(variable.id) + # Find usage in given variation + variable_usage = None + if variable_usages: + variable_usage = variable_usages.get(variable.id) - if variable_usage: - variable_value = variable_usage.value - self.logger.info('Value for variable "%s" for variation "%s" is "%s".' % ( - variable.key, - variation.key, - variable_value - )) + if variable_usage: + variable_value = variable_usage.value + self.logger.info( + 'Value for variable "%s" for variation "%s" is "%s".' % (variable.key, variation.key, variable_value) + ) - else: - variable_value = variable.defaultValue - self.logger.info('Variable "%s" is not used in variation "%s". Assigning default value "%s".' % ( - variable.key, - variation.key, - variable_value - )) + else: + variable_value = variable.defaultValue + self.logger.info( + 'Variable "%s" is not used in variation "%s". Assigning default value "%s".' + % (variable.key, variation.key, variable_value) + ) - return variable_value + return variable_value - def get_variable_for_feature(self, feature_key, variable_key): - """ Get the variable with the given variable key for the given feature. + def get_variable_for_feature(self, feature_key, variable_key): + """ Get the variable with the given variable key for the given feature. Args: feature_key: The key of the feature for which we are getting the variable. @@ -478,37 +477,37 @@ def get_variable_for_feature(self, feature_key, variable_key): Returns: Variable with the given key in the given variation. """ - feature = self.feature_key_map.get(feature_key) - if not feature: - self.logger.error('Feature with key "%s" not found in the datafile.' % feature_key) - return None + feature = self.feature_key_map.get(feature_key) + if not feature: + self.logger.error('Feature with key "%s" not found in the datafile.' % feature_key) + return None - if variable_key not in feature.variables: - self.logger.error('Variable with key "%s" not found in the datafile.' % variable_key) - return None + if variable_key not in feature.variables: + self.logger.error('Variable with key "%s" not found in the datafile.' % variable_key) + return None - return feature.variables.get(variable_key) + return feature.variables.get(variable_key) - def get_anonymize_ip_value(self): - """ Gets the anonymize IP value. + def get_anonymize_ip_value(self): + """ Gets the anonymize IP value. Returns: A boolean value that indicates if the IP should be anonymized. """ - return self.anonymize_ip + return self.anonymize_ip - def get_bot_filtering_value(self): - """ Gets the bot filtering value. + def get_bot_filtering_value(self): + """ Gets the bot filtering value. Returns: A boolean value that indicates if bot filtering should be enabled. """ - return self.bot_filtering + return self.bot_filtering - def is_feature_experiment(self, experiment_id): - """ Determines if given experiment is a feature test. + def is_feature_experiment(self, experiment_id): + """ Determines if given experiment is a feature test. Args: experiment_id: Experiment ID for which feature test is to be determined. @@ -517,4 +516,4 @@ def is_feature_experiment(self, experiment_id): A boolean value that indicates if given experiment is a feature test. """ - return experiment_id in self.experiment_feature_map + return experiment_id in self.experiment_feature_map diff --git a/optimizely/user_profile.py b/optimizely/user_profile.py index 67452dd4..177bfc7c 100644 --- a/optimizely/user_profile.py +++ b/optimizely/user_profile.py @@ -13,26 +13,26 @@ class UserProfile(object): - """ Class encapsulating information representing a user's profile. + """ Class encapsulating information representing a user's profile. user_id: User's identifier. experiment_bucket_map: Dict mapping experiment ID to dict consisting of the variation ID identifying the variation for the user. """ - USER_ID_KEY = 'user_id' - EXPERIMENT_BUCKET_MAP_KEY = 'experiment_bucket_map' - VARIATION_ID_KEY = 'variation_id' + USER_ID_KEY = 'user_id' + EXPERIMENT_BUCKET_MAP_KEY = 'experiment_bucket_map' + VARIATION_ID_KEY = 'variation_id' - def __init__(self, user_id, experiment_bucket_map=None, **kwargs): - self.user_id = user_id - self.experiment_bucket_map = experiment_bucket_map or {} + def __init__(self, user_id, experiment_bucket_map=None, **kwargs): + self.user_id = user_id + self.experiment_bucket_map = experiment_bucket_map or {} - def __eq__(self, other): - return self.__dict__ == other.__dict__ + def __eq__(self, other): + return self.__dict__ == other.__dict__ - def get_variation_for_experiment(self, experiment_id): - """ Helper method to retrieve variation ID for given experiment. + def get_variation_for_experiment(self, experiment_id): + """ Helper method to retrieve variation ID for given experiment. Args: experiment_id: ID for experiment for which variation needs to be looked up for. @@ -41,29 +41,25 @@ def get_variation_for_experiment(self, experiment_id): Variation ID corresponding to the experiment. None if no decision available. """ - return self.experiment_bucket_map.get(experiment_id, {self.VARIATION_ID_KEY: None}).get(self.VARIATION_ID_KEY) + return self.experiment_bucket_map.get(experiment_id, {self.VARIATION_ID_KEY: None}).get(self.VARIATION_ID_KEY) - def save_variation_for_experiment(self, experiment_id, variation_id): - """ Helper method to save new experiment/variation as part of the user's profile. + def save_variation_for_experiment(self, experiment_id, variation_id): + """ Helper method to save new experiment/variation as part of the user's profile. Args: experiment_id: ID for experiment for which the decision is to be stored. variation_id: ID for variation that the user saw. """ - self.experiment_bucket_map.update({ - experiment_id: { - self.VARIATION_ID_KEY: variation_id - } - }) + self.experiment_bucket_map.update({experiment_id: {self.VARIATION_ID_KEY: variation_id}}) class UserProfileService(object): - """ Class encapsulating user profile service functionality. + """ Class encapsulating user profile service functionality. Override with your own implementation for storing and retrieving the user profile. """ - def lookup(self, user_id): - """ Fetch the user profile dict corresponding to the user ID. + def lookup(self, user_id): + """ Fetch the user profile dict corresponding to the user ID. Args: user_id: ID for user whose profile needs to be retrieved. @@ -71,12 +67,12 @@ def lookup(self, user_id): Returns: Dict representing the user's profile. """ - return UserProfile(user_id).__dict__ + return UserProfile(user_id).__dict__ - def save(self, user_profile): - """ Save the user profile dict sent to this method. + def save(self, user_profile): + """ Save the user profile dict sent to this method. Args: user_profile: Dict representing the user's profile. """ - pass + pass diff --git a/setup.py b/setup.py index f6ac5362..1a17451d 100644 --- a/setup.py +++ b/setup.py @@ -8,25 +8,27 @@ __version__ = None with open(os.path.join(here, 'optimizely', 'version.py')) as _file: - exec(_file.read()) + exec(_file.read()) with open(os.path.join(here, 'requirements', 'core.txt')) as _file: - REQUIREMENTS = _file.read().splitlines() + REQUIREMENTS = _file.read().splitlines() with open(os.path.join(here, 'requirements', 'test.txt')) as _file: - TEST_REQUIREMENTS = _file.read().splitlines() - TEST_REQUIREMENTS = list(set(REQUIREMENTS + TEST_REQUIREMENTS)) + TEST_REQUIREMENTS = _file.read().splitlines() + TEST_REQUIREMENTS = list(set(REQUIREMENTS + TEST_REQUIREMENTS)) with open(os.path.join(here, 'README.md')) as _file: - README = _file.read() + README = _file.read() with open(os.path.join(here, 'CHANGELOG.md')) as _file: - CHANGELOG = _file.read() + CHANGELOG = _file.read() -about_text = 'Optimizely X Full Stack is A/B testing and feature management for product development teams. ' \ - 'Experiment in any application. Make every feature on your roadmap an opportunity to learn. ' \ - 'Learn more at https://www.optimizely.com/products/full-stack/ or see our documentation at ' \ - 'https://developers.optimizely.com/x/solutions/sdks/reference/index.html?language=python.' +about_text = ( + 'Optimizely X Full Stack is A/B testing and feature management for product development teams. ' + 'Experiment in any application. Make every feature on your roadmap an opportunity to learn. ' + 'Learn more at https://www.optimizely.com/products/full-stack/ or see our documentation at ' + 'https://developers.optimizely.com/x/solutions/sdks/reference/index.html?language=python.' +) setup( name='optimizely-sdk', @@ -38,22 +40,20 @@ author_email='developers@optimizely.com', url='https://github.com/optimizely/python-sdk', classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Intended Audience :: Developers', - 'License :: OSI Approved :: Apache Software License', - 'Operating System :: OS Independent', - 'Programming Language :: Python', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3.4', - 'Programming Language :: Python :: 3.5', - 'Programming Language :: Python :: 3.6' + 'Development Status :: 5 - Production/Stable', + 'Environment :: Web Environment', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: Apache Software License', + 'Operating System :: OS Independent', + 'Programming Language :: Python', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3.4', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', ], - packages=find_packages( - exclude=['tests'] - ), + packages=find_packages(exclude=['tests']), extras_require={'test': TEST_REQUIREMENTS}, install_requires=REQUIREMENTS, tests_require=TEST_REQUIREMENTS, - test_suite='tests' + test_suite='tests', ) diff --git a/tests/base.py b/tests/base.py index 57e31738..2b2e2802 100644 --- a/tests/base.py +++ b/tests/base.py @@ -18,1064 +18,833 @@ from optimizely import optimizely if PY3: - def long(a): - raise NotImplementedError('Tests should only call `long` if running in PY2') + def long(a): + raise NotImplementedError('Tests should only call `long` if running in PY2') -class BaseTest(unittest.TestCase): - - def assertStrictTrue(self, to_assert): - self.assertIs(to_assert, True) - - def assertStrictFalse(self, to_assert): - self.assertIs(to_assert, False) - - def setUp(self, config_dict='config_dict'): - self.config_dict = { - 'revision': '42', - 'version': '2', - 'events': [{ - 'key': 'test_event', - 'experimentIds': ['111127'], - 'id': '111095' - }, { - 'key': 'Total Revenue', - 'experimentIds': ['111127'], - 'id': '111096' - }], - 'experiments': [{ - 'key': 'test_experiment', - 'status': 'Running', - 'forcedVariations': { - 'user_1': 'control', - 'user_2': 'control' - }, - 'layerId': '111182', - 'audienceIds': ['11154'], - 'trafficAllocation': [{ - 'entityId': '111128', - 'endOfRange': 4000 - }, { - 'entityId': '', - 'endOfRange': 5000 - }, { - 'entityId': '111129', - 'endOfRange': 9000 - }], - 'id': '111127', - 'variations': [{ - 'key': 'control', - 'id': '111128' - }, { - 'key': 'variation', - 'id': '111129' - }] - }], - 'groups': [{ - 'id': '19228', - 'policy': 'random', - 'experiments': [{ - 'id': '32222', - 'key': 'group_exp_1', - 'status': 'Running', - 'audienceIds': [], - 'layerId': '111183', - 'variations': [{ - 'key': 'group_exp_1_control', - 'id': '28901' - }, { - 'key': 'group_exp_1_variation', - 'id': '28902' - }], - 'forcedVariations': { - 'user_1': 'group_exp_1_control', - 'user_2': 'group_exp_1_control' - }, - 'trafficAllocation': [{ - 'entityId': '28901', - 'endOfRange': 3000 - }, { - 'entityId': '28902', - 'endOfRange': 9000 - }] - }, { - 'id': '32223', - 'key': 'group_exp_2', - 'status': 'Running', - 'audienceIds': [], - 'layerId': '111184', - 'variations': [{ - 'key': 'group_exp_2_control', - 'id': '28905' - }, { - 'key': 'group_exp_2_variation', - 'id': '28906' - }], - 'forcedVariations': { - 'user_1': 'group_exp_2_control', - 'user_2': 'group_exp_2_control' - }, - 'trafficAllocation': [{ - 'entityId': '28905', - 'endOfRange': 8000 - }, { - 'entityId': '28906', - 'endOfRange': 10000 - }] - }], - 'trafficAllocation': [{ - 'entityId': '32222', - "endOfRange": 3000 - }, { - 'entityId': '32223', - 'endOfRange': 7500 - }] - }], - 'accountId': '12001', - 'attributes': [{ - 'key': 'test_attribute', - 'id': '111094' - }, { - 'key': 'boolean_key', - 'id': '111196' - }, { - 'key': 'integer_key', - 'id': '111197' - }, { - 'key': 'double_key', - 'id': '111198' - }], - 'audiences': [{ - 'name': 'Test attribute users 1', - 'conditions': '["and", ["or", ["or", ' - '{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_1"}]]]', - 'id': '11154' - }, { - 'name': 'Test attribute users 2', - 'conditions': '["and", ["or", ["or", ' - '{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_2"}]]]', - 'id': '11159' - }], - 'projectId': '111001' - } - # datafile version 4 - self.config_dict_with_features = { - 'revision': '1', - 'accountId': '12001', - 'projectId': '111111', - 'version': '4', - 'botFiltering': True, - 'events': [{ - 'key': 'test_event', - 'experimentIds': ['111127'], - 'id': '111095' - }], - 'experiments': [{ - 'key': 'test_experiment', - 'status': 'Running', - 'forcedVariations': {}, - 'layerId': '111182', - 'audienceIds': [], - 'trafficAllocation': [{ - 'entityId': '111128', - 'endOfRange': 5000 - }, { - 'entityId': '111129', - 'endOfRange': 9000 - }], - 'id': '111127', - 'variations': [{ - 'key': 'control', - 'id': '111128', - 'featureEnabled': False, - 'variables': [{ - 'id': '127', 'value': 'false' - }, { - 'id': '128', 'value': 'prod' - }, { - 'id': '129', 'value': '10.01' - }, { - 'id': '130', 'value': '4242' - }] - }, { - 'key': 'variation', - 'id': '111129', - 'featureEnabled': True, - 'variables': [{ - 'id': '127', 'value': 'true' - }, { - 'id': '128', 'value': 'staging' - }, { - 'id': '129', 'value': '10.02' - }, { - 'id': '130', 'value': '4243' - }] - }] - }, { - 'key': 'test_experiment2', - 'status': 'Running', - 'layerId': '5', - 'audienceIds': [], - 'id': '111133', - 'forcedVariations': {}, - 'trafficAllocation': [{ - 'entityId': '122239', - 'endOfRange': 5000 - }, { - 'entityId': '122240', - 'endOfRange': 10000 - }], - 'variations': [{ - 'id': '122239', - 'key': 'control', - 'featureEnabled': True, - 'variables': [ - { - 'id': '155551', - 'value': '42.42' - } - ] - }, { - 'id': '122240', - 'key': 'variation', - 'featureEnabled': True, - 'variables': [ - { - 'id': '155551', - 'value': '13.37' - } - ] - }] - }], - 'groups': [{ - 'id': '19228', - 'policy': 'random', - 'experiments': [{ - 'id': '32222', - 'key': 'group_exp_1', - 'status': 'Running', - 'audienceIds': [], - 'layerId': '111183', - 'variations': [{ - 'key': 'group_exp_1_control', - 'id': '28901' - }, { - 'key': 'group_exp_1_variation', - 'id': '28902' - }], - 'forcedVariations': { - 'user_1': 'group_exp_1_control', - 'user_2': 'group_exp_1_control' - }, - 'trafficAllocation': [{ - 'entityId': '28901', - 'endOfRange': 3000 - }, { - 'entityId': '28902', - 'endOfRange': 9000 - }] - }, { - 'id': '32223', - 'key': 'group_exp_2', - 'status': 'Running', - 'audienceIds': [], - 'layerId': '111184', - 'variations': [{ - 'key': 'group_exp_2_control', - 'id': '28905' - }, { - 'key': 'group_exp_2_variation', - 'id': '28906' - }], - 'forcedVariations': { - 'user_1': 'group_exp_2_control', - 'user_2': 'group_exp_2_control' - }, - 'trafficAllocation': [{ - 'entityId': '28905', - 'endOfRange': 8000 - }, { - 'entityId': '28906', - 'endOfRange': 10000 - }] - }], - 'trafficAllocation': [{ - 'entityId': '32222', - "endOfRange": 3000 - }, { - 'entityId': '32223', - 'endOfRange': 7500 - }] - }], - 'attributes': [{ - 'key': 'test_attribute', - 'id': '111094' - }], - 'audiences': [{ - 'name': 'Test attribute users 1', - 'conditions': '["and", ["or", ["or", ' - '{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_1"}]]]', - 'id': '11154' - }, { - 'name': 'Test attribute users 2', - 'conditions': '["and", ["or", ["or", ' - '{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_2"}]]]', - 'id': '11159' - }], - 'rollouts': [{ - 'id': '201111', - 'experiments': [] - }, { - 'id': '211111', - 'experiments': [{ - 'id': '211127', - 'key': '211127', - 'status': 'Running', - 'forcedVariations': {}, - 'layerId': '211111', - 'audienceIds': ['11154'], - 'trafficAllocation': [{ - 'entityId': '211129', - 'endOfRange': 9000 - }], - 'variations': [{ - 'key': '211129', - 'id': '211129', - 'featureEnabled': True, - 'variables': [{ - 'id': '132', 'value': 'true' - }, { - 'id': '133', 'value': 'Hello audience' - }, { - 'id': '134', 'value': '39.99' - }, { - 'id': '135', 'value': '399' - }] - }, { - 'key': '211229', - 'id': '211229', - 'featureEnabled': False, - 'variables': [{ - 'id': '132', 'value': 'true' - }, { - 'id': '133', 'value': 'environment' - }, { - 'id': '134', 'value': '49.99' - }, { - 'id': '135', 'value': '499' - }] - }] - }, { - 'id': '211137', - 'key': '211137', - 'status': 'Running', - 'forcedVariations': {}, - 'layerId': '211111', - 'audienceIds': ['11159'], - 'trafficAllocation': [{ - 'entityId': '211139', - 'endOfRange': 3000 - }], - 'variations': [{ - 'key': '211139', - 'id': '211139', - 'featureEnabled': True - }] - }, { - 'id': '211147', - 'key': '211147', - 'status': 'Running', - 'forcedVariations': {}, - 'layerId': '211111', - 'audienceIds': [], - 'trafficAllocation': [{ - 'entityId': '211149', - 'endOfRange': 6000 - }], - 'variations': [{ - 'key': '211149', - 'id': '211149', - 'featureEnabled': True - }] - }] - }], - 'featureFlags': [{ - 'id': '91111', - 'key': 'test_feature_in_experiment', - 'experimentIds': ['111127'], - 'rolloutId': '', - 'variables': [{ - 'id': '127', - 'key': 'is_working', - 'defaultValue': 'true', - 'type': 'boolean', - }, { - 'id': '128', - 'key': 'environment', - 'defaultValue': 'devel', - 'type': 'string', - }, { - 'id': '129', - 'key': 'cost', - 'defaultValue': '10.99', - 'type': 'double', - }, { - 'id': '130', - 'key': 'count', - 'defaultValue': '999', - 'type': 'integer', - }, { - 'id': '131', - 'key': 'variable_without_usage', - 'defaultValue': '45', - 'type': 'integer', - }] - }, { - 'id': '91112', - 'key': 'test_feature_in_rollout', - 'experimentIds': [], - 'rolloutId': '211111', - 'variables': [{ - 'id': '132', - 'key': 'is_running', - 'defaultValue': 'false', - 'type': 'boolean' - }, { - 'id': '133', - 'key': 'message', - 'defaultValue': 'Hello', - 'type': 'string' - }, { - 'id': '134', - 'key': 'price', - 'defaultValue': '99.99', - 'type': 'double' - }, { - 'id': '135', - 'key': 'count', - 'defaultValue': '999', - 'type': 'integer' - }] - }, { - 'id': '91113', - 'key': 'test_feature_in_group', - 'experimentIds': ['32222'], - 'rolloutId': '', - 'variables': [], - }, { - 'id': '91114', - 'key': 'test_feature_in_experiment_and_rollout', - 'experimentIds': ['32223'], - 'rolloutId': '211111', - 'variables': [], - }] - } - - self.config_dict_with_multiple_experiments = { - 'revision': '42', - 'version': '2', - 'events': [{ - 'key': 'test_event', - 'experimentIds': ['111127', '111130'], - 'id': '111095' - }, { - 'key': 'Total Revenue', - 'experimentIds': ['111127'], - 'id': '111096' - }], - 'experiments': [{ - 'key': 'test_experiment', - 'status': 'Running', - 'forcedVariations': { - 'user_1': 'control', - 'user_2': 'control' - }, - 'layerId': '111182', - 'audienceIds': ['11154'], - 'trafficAllocation': [{ - 'entityId': '111128', - 'endOfRange': 4000 - }, { - 'entityId': '', - 'endOfRange': 5000 - }, { - 'entityId': '111129', - 'endOfRange': 9000 - }], - 'id': '111127', - 'variations': [{ - 'key': 'control', - 'id': '111128' - }, { - 'key': 'variation', - 'id': '111129' - }] - }, { - 'key': 'test_experiment_2', - 'status': 'Running', - 'forcedVariations': { - 'user_1': 'control', - 'user_2': 'control' - }, - 'layerId': '111182', - 'audienceIds': ['11154'], - 'trafficAllocation': [{ - 'entityId': '111131', - 'endOfRange': 4000 - }, { - 'entityId': '', - 'endOfRange': 5000 - }, { - 'entityId': '111132', - 'endOfRange': 9000 - }], - 'id': '111130', - 'variations': [{ - 'key': 'control', - 'id': '111131' - }, { - 'key': 'variation', - 'id': '111132' - }] - }], - 'groups': [{ - 'id': '19228', - 'policy': 'random', - 'experiments': [{ - 'id': '32222', - 'key': 'group_exp_1', - 'status': 'Running', - 'audienceIds': [], - 'layerId': '111183', - 'variations': [{ - 'key': 'group_exp_1_control', - 'id': '28901' - }, { - 'key': 'group_exp_1_variation', - 'id': '28902' - }], - 'forcedVariations': { - 'user_1': 'group_exp_1_control', - 'user_2': 'group_exp_1_control' - }, - 'trafficAllocation': [{ - 'entityId': '28901', - 'endOfRange': 3000 - }, { - 'entityId': '28902', - 'endOfRange': 9000 - }] - }, { - 'id': '32223', - 'key': 'group_exp_2', - 'status': 'Running', - 'audienceIds': [], - 'layerId': '111184', - 'variations': [{ - 'key': 'group_exp_2_control', - 'id': '28905' - }, { - 'key': 'group_exp_2_variation', - 'id': '28906' - }], - 'forcedVariations': { - 'user_1': 'group_exp_2_control', - 'user_2': 'group_exp_2_control' - }, - 'trafficAllocation': [{ - 'entityId': '28905', - 'endOfRange': 8000 - }, { - 'entityId': '28906', - 'endOfRange': 10000 - }] - }], - 'trafficAllocation': [{ - 'entityId': '32222', - "endOfRange": 3000 - }, { - 'entityId': '32223', - 'endOfRange': 7500 - }] - }], - 'accountId': '12001', - 'attributes': [{ - 'key': 'test_attribute', - 'id': '111094' - }, { - 'key': 'boolean_key', - 'id': '111196' - }, { - 'key': 'integer_key', - 'id': '111197' - }, { - 'key': 'double_key', - 'id': '111198' - }], - 'audiences': [{ - 'name': 'Test attribute users 1', - 'conditions': '["and", ["or", ["or", ' - '{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_1"}]]]', - 'id': '11154' - }, { - 'name': 'Test attribute users 2', - 'conditions': '["and", ["or", ["or", ' - '{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_2"}]]]', - 'id': '11159' - }], - 'projectId': '111001' - } +class BaseTest(unittest.TestCase): + def assertStrictTrue(self, to_assert): + self.assertIs(to_assert, True) - self.config_dict_with_unsupported_version = { - 'version': '5', - 'rollouts': [], - 'projectId': '10431130345', - 'variables': [], - 'featureFlags': [], - 'experiments': [ - { - 'status': 'Running', - 'key': 'ab_running_exp_untargeted', - 'layerId': '10417730432', - 'trafficAllocation': [ - { - 'entityId': '10418551353', - 'endOfRange': 10000 - } - ], - 'audienceIds': [], - 'variations': [ - { - 'variables': [], - 'id': '10418551353', - 'key': 'all_traffic_variation' - }, - { - 'variables': [], - 'id': '10418510624', - 'key': 'no_traffic_variation' - } - ], - 'forcedVariations': {}, - 'id': '10420810910' - } - ], - 'audiences': [], - 'groups': [], - 'attributes': [], - 'accountId': '10367498574', - 'events': [ - { - 'experimentIds': [ - '10420810910' - ], - 'id': '10404198134', - 'key': 'winning' - } - ], - 'revision': '1337' - } + def assertStrictFalse(self, to_assert): + self.assertIs(to_assert, False) - self.config_dict_with_typed_audiences = { - 'version': '4', - 'rollouts': [ - { - 'experiments': [ - { - 'status': 'Running', - 'key': '11488548027', - 'layerId': '11551226731', - 'trafficAllocation': [ + def setUp(self, config_dict='config_dict'): + self.config_dict = { + 'revision': '42', + 'version': '2', + 'events': [ + {'key': 'test_event', 'experimentIds': ['111127'], 'id': '111095'}, + {'key': 'Total Revenue', 'experimentIds': ['111127'], 'id': '111096'}, + ], + 'experiments': [ { - 'entityId': '11557362669', - 'endOfRange': 10000 + 'key': 'test_experiment', + 'status': 'Running', + 'forcedVariations': {'user_1': 'control', 'user_2': 'control'}, + 'layerId': '111182', + 'audienceIds': ['11154'], + 'trafficAllocation': [ + {'entityId': '111128', 'endOfRange': 4000}, + {'entityId': '', 'endOfRange': 5000}, + {'entityId': '111129', 'endOfRange': 9000}, + ], + 'id': '111127', + 'variations': [{'key': 'control', 'id': '111128'}, {'key': 'variation', 'id': '111129'}], } - ], - 'audienceIds': ['3468206642', '3988293898', '3988293899', '3468206646', - '3468206647', '3468206644', '3468206643'], - 'variations': [ + ], + 'groups': [ { - 'variables': [], - 'id': '11557362669', - 'key': '11557362669', - 'featureEnabled':True + 'id': '19228', + 'policy': 'random', + 'experiments': [ + { + 'id': '32222', + 'key': 'group_exp_1', + 'status': 'Running', + 'audienceIds': [], + 'layerId': '111183', + 'variations': [ + {'key': 'group_exp_1_control', 'id': '28901'}, + {'key': 'group_exp_1_variation', 'id': '28902'}, + ], + 'forcedVariations': {'user_1': 'group_exp_1_control', 'user_2': 'group_exp_1_control'}, + 'trafficAllocation': [ + {'entityId': '28901', 'endOfRange': 3000}, + {'entityId': '28902', 'endOfRange': 9000}, + ], + }, + { + 'id': '32223', + 'key': 'group_exp_2', + 'status': 'Running', + 'audienceIds': [], + 'layerId': '111184', + 'variations': [ + {'key': 'group_exp_2_control', 'id': '28905'}, + {'key': 'group_exp_2_variation', 'id': '28906'}, + ], + 'forcedVariations': {'user_1': 'group_exp_2_control', 'user_2': 'group_exp_2_control'}, + 'trafficAllocation': [ + {'entityId': '28905', 'endOfRange': 8000}, + {'entityId': '28906', 'endOfRange': 10000}, + ], + }, + ], + 'trafficAllocation': [ + {'entityId': '32222', "endOfRange": 3000}, + {'entityId': '32223', 'endOfRange': 7500}, + ], } - ], - 'forcedVariations': {}, - 'id': '11488548027' - } - ], - 'id': '11551226731' - }, - { - 'experiments': [ - { - 'status': 'Paused', - 'key': '11630490911', - 'layerId': '11638870867', - 'trafficAllocation': [ + ], + 'accountId': '12001', + 'attributes': [ + {'key': 'test_attribute', 'id': '111094'}, + {'key': 'boolean_key', 'id': '111196'}, + {'key': 'integer_key', 'id': '111197'}, + {'key': 'double_key', 'id': '111198'}, + ], + 'audiences': [ { - 'entityId': '11475708558', - 'endOfRange': 0 - } - ], - 'audienceIds': [], - 'variations': [ + 'name': 'Test attribute users 1', + 'conditions': '["and", ["or", ["or", ' + '{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_1"}]]]', + 'id': '11154', + }, { - 'variables': [], - 'id': '11475708558', - 'key': '11475708558', - 'featureEnabled':False - } - ], - 'forcedVariations': {}, - 'id': '11630490911' - } - ], - 'id': '11638870867' - }, - { - 'experiments': [ - { - 'status': 'Running', - 'key': '11488548028', - 'layerId': '11551226732', - 'trafficAllocation': [ + 'name': 'Test attribute users 2', + 'conditions': '["and", ["or", ["or", ' + '{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_2"}]]]', + 'id': '11159', + }, + ], + 'projectId': '111001', + } + + # datafile version 4 + self.config_dict_with_features = { + 'revision': '1', + 'accountId': '12001', + 'projectId': '111111', + 'version': '4', + 'botFiltering': True, + 'events': [{'key': 'test_event', 'experimentIds': ['111127'], 'id': '111095'}], + 'experiments': [ { - 'entityId': '11557362670', - 'endOfRange': 10000 - } - ], - 'audienceIds': ['0'], - 'audienceConditions': ['and', ['or', '3468206642', '3988293898'], ['or', '3988293899', - '3468206646', '3468206647', '3468206644', '3468206643']], - 'variations': [ + 'key': 'test_experiment', + 'status': 'Running', + 'forcedVariations': {}, + 'layerId': '111182', + 'audienceIds': [], + 'trafficAllocation': [ + {'entityId': '111128', 'endOfRange': 5000}, + {'entityId': '111129', 'endOfRange': 9000}, + ], + 'id': '111127', + 'variations': [ + { + 'key': 'control', + 'id': '111128', + 'featureEnabled': False, + 'variables': [ + {'id': '127', 'value': 'false'}, + {'id': '128', 'value': 'prod'}, + {'id': '129', 'value': '10.01'}, + {'id': '130', 'value': '4242'}, + ], + }, + { + 'key': 'variation', + 'id': '111129', + 'featureEnabled': True, + 'variables': [ + {'id': '127', 'value': 'true'}, + {'id': '128', 'value': 'staging'}, + {'id': '129', 'value': '10.02'}, + {'id': '130', 'value': '4243'}, + ], + }, + ], + }, { - 'variables': [], - 'id': '11557362670', - 'key': '11557362670', - 'featureEnabled': True - } - ], - 'forcedVariations': {}, - 'id': '11488548028' - } - ], - 'id': '11551226732' - }, - { - 'experiments': [ - { - 'status': 'Paused', - 'key': '11630490912', - 'layerId': '11638870868', - 'trafficAllocation': [ + 'key': 'test_experiment2', + 'status': 'Running', + 'layerId': '5', + 'audienceIds': [], + 'id': '111133', + 'forcedVariations': {}, + 'trafficAllocation': [ + {'entityId': '122239', 'endOfRange': 5000}, + {'entityId': '122240', 'endOfRange': 10000}, + ], + 'variations': [ + { + 'id': '122239', + 'key': 'control', + 'featureEnabled': True, + 'variables': [{'id': '155551', 'value': '42.42'}], + }, + { + 'id': '122240', + 'key': 'variation', + 'featureEnabled': True, + 'variables': [{'id': '155551', 'value': '13.37'}], + }, + ], + }, + ], + 'groups': [ { - 'entityId': '11475708559', - 'endOfRange': 0 + 'id': '19228', + 'policy': 'random', + 'experiments': [ + { + 'id': '32222', + 'key': 'group_exp_1', + 'status': 'Running', + 'audienceIds': [], + 'layerId': '111183', + 'variations': [ + {'key': 'group_exp_1_control', 'id': '28901'}, + {'key': 'group_exp_1_variation', 'id': '28902'}, + ], + 'forcedVariations': {'user_1': 'group_exp_1_control', 'user_2': 'group_exp_1_control'}, + 'trafficAllocation': [ + {'entityId': '28901', 'endOfRange': 3000}, + {'entityId': '28902', 'endOfRange': 9000}, + ], + }, + { + 'id': '32223', + 'key': 'group_exp_2', + 'status': 'Running', + 'audienceIds': [], + 'layerId': '111184', + 'variations': [ + {'key': 'group_exp_2_control', 'id': '28905'}, + {'key': 'group_exp_2_variation', 'id': '28906'}, + ], + 'forcedVariations': {'user_1': 'group_exp_2_control', 'user_2': 'group_exp_2_control'}, + 'trafficAllocation': [ + {'entityId': '28905', 'endOfRange': 8000}, + {'entityId': '28906', 'endOfRange': 10000}, + ], + }, + ], + 'trafficAllocation': [ + {'entityId': '32222', "endOfRange": 3000}, + {'entityId': '32223', 'endOfRange': 7500}, + ], } - ], - 'audienceIds': [], - 'variations': [ + ], + 'attributes': [{'key': 'test_attribute', 'id': '111094'}], + 'audiences': [ { - 'variables': [], - 'id': '11475708559', - 'key': '11475708559', - 'featureEnabled': False - } - ], - 'forcedVariations': {}, - 'id': '11630490912' - } - ], - 'id': '11638870868' + 'name': 'Test attribute users 1', + 'conditions': '["and", ["or", ["or", ' + '{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_1"}]]]', + 'id': '11154', + }, + { + 'name': 'Test attribute users 2', + 'conditions': '["and", ["or", ["or", ' + '{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_2"}]]]', + 'id': '11159', + }, + ], + 'rollouts': [ + {'id': '201111', 'experiments': []}, + { + 'id': '211111', + 'experiments': [ + { + 'id': '211127', + 'key': '211127', + 'status': 'Running', + 'forcedVariations': {}, + 'layerId': '211111', + 'audienceIds': ['11154'], + 'trafficAllocation': [{'entityId': '211129', 'endOfRange': 9000}], + 'variations': [ + { + 'key': '211129', + 'id': '211129', + 'featureEnabled': True, + 'variables': [ + {'id': '132', 'value': 'true'}, + {'id': '133', 'value': 'Hello audience'}, + {'id': '134', 'value': '39.99'}, + {'id': '135', 'value': '399'}, + ], + }, + { + 'key': '211229', + 'id': '211229', + 'featureEnabled': False, + 'variables': [ + {'id': '132', 'value': 'true'}, + {'id': '133', 'value': 'environment'}, + {'id': '134', 'value': '49.99'}, + {'id': '135', 'value': '499'}, + ], + }, + ], + }, + { + 'id': '211137', + 'key': '211137', + 'status': 'Running', + 'forcedVariations': {}, + 'layerId': '211111', + 'audienceIds': ['11159'], + 'trafficAllocation': [{'entityId': '211139', 'endOfRange': 3000}], + 'variations': [{'key': '211139', 'id': '211139', 'featureEnabled': True}], + }, + { + 'id': '211147', + 'key': '211147', + 'status': 'Running', + 'forcedVariations': {}, + 'layerId': '211111', + 'audienceIds': [], + 'trafficAllocation': [{'entityId': '211149', 'endOfRange': 6000}], + 'variations': [{'key': '211149', 'id': '211149', 'featureEnabled': True}], + }, + ], + }, + ], + 'featureFlags': [ + { + 'id': '91111', + 'key': 'test_feature_in_experiment', + 'experimentIds': ['111127'], + 'rolloutId': '', + 'variables': [ + {'id': '127', 'key': 'is_working', 'defaultValue': 'true', 'type': 'boolean'}, + {'id': '128', 'key': 'environment', 'defaultValue': 'devel', 'type': 'string'}, + {'id': '129', 'key': 'cost', 'defaultValue': '10.99', 'type': 'double'}, + {'id': '130', 'key': 'count', 'defaultValue': '999', 'type': 'integer'}, + {'id': '131', 'key': 'variable_without_usage', 'defaultValue': '45', 'type': 'integer'}, + ], + }, + { + 'id': '91112', + 'key': 'test_feature_in_rollout', + 'experimentIds': [], + 'rolloutId': '211111', + 'variables': [ + {'id': '132', 'key': 'is_running', 'defaultValue': 'false', 'type': 'boolean'}, + {'id': '133', 'key': 'message', 'defaultValue': 'Hello', 'type': 'string'}, + {'id': '134', 'key': 'price', 'defaultValue': '99.99', 'type': 'double'}, + {'id': '135', 'key': 'count', 'defaultValue': '999', 'type': 'integer'}, + ], + }, + { + 'id': '91113', + 'key': 'test_feature_in_group', + 'experimentIds': ['32222'], + 'rolloutId': '', + 'variables': [], + }, + { + 'id': '91114', + 'key': 'test_feature_in_experiment_and_rollout', + 'experimentIds': ['32223'], + 'rolloutId': '211111', + 'variables': [], + }, + ], } - ], - 'anonymizeIP': False, - 'projectId': '11624721371', - 'variables': [], - 'featureFlags': [ - { - 'experimentIds': [], - 'rolloutId': '11551226731', - 'variables': [], - 'id': '11477755619', - 'key': 'feat' - }, - { - 'experimentIds': [ - '11564051718' - ], - 'rolloutId': '11638870867', - 'variables': [ - { - 'defaultValue': 'x', - 'type': 'string', - 'id': '11535264366', - 'key': 'x' - } - ], - 'id': '11567102051', - 'key': 'feat_with_var' - }, - { - 'experimentIds': [], - 'rolloutId': '11551226732', - 'variables': [], - 'id': '11567102052', - 'key': 'feat2' - }, - { - 'experimentIds': ['1323241599'], - 'rolloutId': '11638870868', - 'variables': [ - { - 'defaultValue': '10', - 'type': 'integer', - 'id': '11535264367', - 'key': 'z' - } - ], - 'id': '11567102053', - 'key': 'feat2_with_var' - } - ], - 'experiments': [ - { - 'status': 'Running', - 'key': 'feat_with_var_test', - 'layerId': '11504144555', - 'trafficAllocation': [ - { - 'entityId': '11617170975', - 'endOfRange': 10000 - } - ], - 'audienceIds': ['3468206642', '3988293898', '3988293899', '3468206646', - '3468206647', '3468206644', '3468206643'], - 'variations': [ - { - 'variables': [ + self.config_dict_with_multiple_experiments = { + 'revision': '42', + 'version': '2', + 'events': [ + {'key': 'test_event', 'experimentIds': ['111127', '111130'], 'id': '111095'}, + {'key': 'Total Revenue', 'experimentIds': ['111127'], 'id': '111096'}, + ], + 'experiments': [ { - 'id': '11535264366', - 'value': 'xyz' - } - ], - 'id': '11617170975', - 'key': 'variation_2', - 'featureEnabled': True - } - ], - 'forcedVariations': {}, - 'id': '11564051718' - }, - { - 'id': '1323241597', - 'key': 'typed_audience_experiment', - 'layerId': '1630555627', - 'status': 'Running', - 'variations': [ - { - 'id': '1423767503', - 'key': 'A', - 'variables': [] - } - ], - 'trafficAllocation': [ - { - 'entityId': '1423767503', - 'endOfRange': 10000 - } - ], - 'audienceIds': ['3468206642', '3988293898', '3988293899', '3468206646', - '3468206647', '3468206644', '3468206643'], - 'forcedVariations': {} - }, - { - 'id': '1323241598', - 'key': 'audience_combinations_experiment', - 'layerId': '1323241598', - 'status': 'Running', - 'variations': [ - { - 'id': '1423767504', - 'key': 'A', - 'variables': [] - } - ], - 'trafficAllocation': [ - { - 'entityId': '1423767504', - 'endOfRange': 10000 - } - ], - 'audienceIds': ['0'], - 'audienceConditions': ['and', ['or', '3468206642', '3988293898'], ['or', '3988293899', - '3468206646', '3468206647', '3468206644', '3468206643']], - 'forcedVariations': {} - }, - { - 'id': '1323241599', - 'key': 'feat2_with_var_test', - 'layerId': '1323241600', - 'status': 'Running', - 'variations': [ - { - 'variables': [ + 'key': 'test_experiment', + 'status': 'Running', + 'forcedVariations': {'user_1': 'control', 'user_2': 'control'}, + 'layerId': '111182', + 'audienceIds': ['11154'], + 'trafficAllocation': [ + {'entityId': '111128', 'endOfRange': 4000}, + {'entityId': '', 'endOfRange': 5000}, + {'entityId': '111129', 'endOfRange': 9000}, + ], + 'id': '111127', + 'variations': [{'key': 'control', 'id': '111128'}, {'key': 'variation', 'id': '111129'}], + }, { - 'id': '11535264367', - 'value': '150' + 'key': 'test_experiment_2', + 'status': 'Running', + 'forcedVariations': {'user_1': 'control', 'user_2': 'control'}, + 'layerId': '111182', + 'audienceIds': ['11154'], + 'trafficAllocation': [ + {'entityId': '111131', 'endOfRange': 4000}, + {'entityId': '', 'endOfRange': 5000}, + {'entityId': '111132', 'endOfRange': 9000}, + ], + 'id': '111130', + 'variations': [{'key': 'control', 'id': '111131'}, {'key': 'variation', 'id': '111132'}], + }, + ], + 'groups': [ + { + 'id': '19228', + 'policy': 'random', + 'experiments': [ + { + 'id': '32222', + 'key': 'group_exp_1', + 'status': 'Running', + 'audienceIds': [], + 'layerId': '111183', + 'variations': [ + {'key': 'group_exp_1_control', 'id': '28901'}, + {'key': 'group_exp_1_variation', 'id': '28902'}, + ], + 'forcedVariations': {'user_1': 'group_exp_1_control', 'user_2': 'group_exp_1_control'}, + 'trafficAllocation': [ + {'entityId': '28901', 'endOfRange': 3000}, + {'entityId': '28902', 'endOfRange': 9000}, + ], + }, + { + 'id': '32223', + 'key': 'group_exp_2', + 'status': 'Running', + 'audienceIds': [], + 'layerId': '111184', + 'variations': [ + {'key': 'group_exp_2_control', 'id': '28905'}, + {'key': 'group_exp_2_variation', 'id': '28906'}, + ], + 'forcedVariations': {'user_1': 'group_exp_2_control', 'user_2': 'group_exp_2_control'}, + 'trafficAllocation': [ + {'entityId': '28905', 'endOfRange': 8000}, + {'entityId': '28906', 'endOfRange': 10000}, + ], + }, + ], + 'trafficAllocation': [ + {'entityId': '32222', "endOfRange": 3000}, + {'entityId': '32223', 'endOfRange': 7500}, + ], } - ], - 'id': '1423767505', - 'key': 'variation_2', - 'featureEnabled': True - } - ], - 'trafficAllocation': [ - { - 'entityId': '1423767505', - 'endOfRange': 10000 - } - ], - 'audienceIds': ['0'], - 'audienceConditions': ['and', ['or', '3468206642', '3988293898'], ['or', '3988293899', '3468206646', - '3468206647', '3468206644', '3468206643']], - 'forcedVariations': {} - }, - ], - 'audiences': [ - { - 'id': '3468206642', - 'name': 'exactString', - 'conditions': '["and", ["or", ["or", {"name": "house", "type": "custom_attribute", "value": "Gryffindor"}]]]' - }, - { - 'id': '3988293898', - 'name': '$$dummySubstringString', - 'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }' - }, - { - 'id': '3988293899', - 'name': '$$dummyExists', - 'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }' - }, - { - 'id': '3468206646', - 'name': '$$dummyExactNumber', - 'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }' - }, - { - 'id': '3468206647', - 'name': '$$dummyGtNumber', - 'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }' - }, - { - 'id': '3468206644', - 'name': '$$dummyLtNumber', - 'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }' - }, - { - 'id': '3468206643', - 'name': '$$dummyExactBoolean', - 'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }' - }, - { - 'id': '3468206645', - 'name': '$$dummyMultipleCustomAttrs', - 'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }' - }, - { - 'id': '0', - 'name': '$$dummy', - 'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }', - } - ], - 'typedAudiences': [ - { - 'id': '3988293898', - 'name': 'substringString', - 'conditions': ['and', ['or', ['or', {'name': 'house', 'type': 'custom_attribute', - 'match': 'substring', 'value': 'Slytherin'}]]] - }, - { - 'id': '3988293899', - 'name': 'exists', - 'conditions': ['and', ['or', ['or', {'name': 'favorite_ice_cream', 'type': 'custom_attribute', - 'match': 'exists'}]]] - }, - { - 'id': '3468206646', - 'name': 'exactNumber', - 'conditions': ['and', ['or', ['or', {'name': 'lasers', 'type': 'custom_attribute', - 'match': 'exact', 'value': 45.5}]]] - }, - { - 'id': '3468206647', - 'name': 'gtNumber', - 'conditions': ['and', ['or', ['or', {'name': 'lasers', 'type': 'custom_attribute', - 'match': 'gt', 'value': 70}]]] - }, - { - 'id': '3468206644', - 'name': 'ltNumber', - 'conditions': ['and', ['or', ['or', {'name': 'lasers', 'type': 'custom_attribute', - 'match': 'lt', 'value': 1.0}]]] - }, - { - 'id': '3468206643', - 'name': 'exactBoolean', - 'conditions': ['and', ['or', ['or', {'name': 'should_do_it', 'type': 'custom_attribute', - 'match': 'exact', 'value': True}]]] - }, - { - 'id': '3468206645', - 'name': 'multiple_custom_attrs', - 'conditions': ["and", ["or", ["or", {"type": "custom_attribute", "name": "browser", "value": "chrome"}, - {"type": "custom_attribute", "name": "browser", "value": "firefox"}]]] + ], + 'accountId': '12001', + 'attributes': [ + {'key': 'test_attribute', 'id': '111094'}, + {'key': 'boolean_key', 'id': '111196'}, + {'key': 'integer_key', 'id': '111197'}, + {'key': 'double_key', 'id': '111198'}, + ], + 'audiences': [ + { + 'name': 'Test attribute users 1', + 'conditions': '["and", ["or", ["or", ' + '{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_1"}]]]', + 'id': '11154', + }, + { + 'name': 'Test attribute users 2', + 'conditions': '["and", ["or", ["or", ' + '{"name": "test_attribute", "type": "custom_attribute", "value": "test_value_2"}]]]', + 'id': '11159', + }, + ], + 'projectId': '111001', } - ], - 'groups': [], - 'attributes': [ - { - 'key': 'house', - 'id': '594015' - }, - { - 'key': 'lasers', - 'id': '594016' - }, - { - 'key': 'should_do_it', - 'id': '594017' - }, - { - 'key': 'favorite_ice_cream', - 'id': '594018' + + self.config_dict_with_unsupported_version = { + 'version': '5', + 'rollouts': [], + 'projectId': '10431130345', + 'variables': [], + 'featureFlags': [], + 'experiments': [ + { + 'status': 'Running', + 'key': 'ab_running_exp_untargeted', + 'layerId': '10417730432', + 'trafficAllocation': [{'entityId': '10418551353', 'endOfRange': 10000}], + 'audienceIds': [], + 'variations': [ + {'variables': [], 'id': '10418551353', 'key': 'all_traffic_variation'}, + {'variables': [], 'id': '10418510624', 'key': 'no_traffic_variation'}, + ], + 'forcedVariations': {}, + 'id': '10420810910', + } + ], + 'audiences': [], + 'groups': [], + 'attributes': [], + 'accountId': '10367498574', + 'events': [{'experimentIds': ['10420810910'], 'id': '10404198134', 'key': 'winning'}], + 'revision': '1337', } - ], - 'botFiltering': False, - 'accountId': '4879520872', - 'events': [ - { - 'key': 'item_bought', - 'id': '594089', - 'experimentIds': [ - '11564051718', - '1323241597' - ] - }, - { - 'key': 'user_signed_up', - 'id': '594090', - 'experimentIds': ['1323241598', '1323241599'], + + self.config_dict_with_typed_audiences = { + 'version': '4', + 'rollouts': [ + { + 'experiments': [ + { + 'status': 'Running', + 'key': '11488548027', + 'layerId': '11551226731', + 'trafficAllocation': [{'entityId': '11557362669', 'endOfRange': 10000}], + 'audienceIds': [ + '3468206642', + '3988293898', + '3988293899', + '3468206646', + '3468206647', + '3468206644', + '3468206643', + ], + 'variations': [ + {'variables': [], 'id': '11557362669', 'key': '11557362669', 'featureEnabled': True} + ], + 'forcedVariations': {}, + 'id': '11488548027', + } + ], + 'id': '11551226731', + }, + { + 'experiments': [ + { + 'status': 'Paused', + 'key': '11630490911', + 'layerId': '11638870867', + 'trafficAllocation': [{'entityId': '11475708558', 'endOfRange': 0}], + 'audienceIds': [], + 'variations': [ + {'variables': [], 'id': '11475708558', 'key': '11475708558', 'featureEnabled': False} + ], + 'forcedVariations': {}, + 'id': '11630490911', + } + ], + 'id': '11638870867', + }, + { + 'experiments': [ + { + 'status': 'Running', + 'key': '11488548028', + 'layerId': '11551226732', + 'trafficAllocation': [{'entityId': '11557362670', 'endOfRange': 10000}], + 'audienceIds': ['0'], + 'audienceConditions': [ + 'and', + ['or', '3468206642', '3988293898'], + ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643'], + ], + 'variations': [ + {'variables': [], 'id': '11557362670', 'key': '11557362670', 'featureEnabled': True} + ], + 'forcedVariations': {}, + 'id': '11488548028', + } + ], + 'id': '11551226732', + }, + { + 'experiments': [ + { + 'status': 'Paused', + 'key': '11630490912', + 'layerId': '11638870868', + 'trafficAllocation': [{'entityId': '11475708559', 'endOfRange': 0}], + 'audienceIds': [], + 'variations': [ + {'variables': [], 'id': '11475708559', 'key': '11475708559', 'featureEnabled': False} + ], + 'forcedVariations': {}, + 'id': '11630490912', + } + ], + 'id': '11638870868', + }, + ], + 'anonymizeIP': False, + 'projectId': '11624721371', + 'variables': [], + 'featureFlags': [ + {'experimentIds': [], 'rolloutId': '11551226731', 'variables': [], 'id': '11477755619', 'key': 'feat'}, + { + 'experimentIds': ['11564051718'], + 'rolloutId': '11638870867', + 'variables': [{'defaultValue': 'x', 'type': 'string', 'id': '11535264366', 'key': 'x'}], + 'id': '11567102051', + 'key': 'feat_with_var', + }, + { + 'experimentIds': [], + 'rolloutId': '11551226732', + 'variables': [], + 'id': '11567102052', + 'key': 'feat2', + }, + { + 'experimentIds': ['1323241599'], + 'rolloutId': '11638870868', + 'variables': [{'defaultValue': '10', 'type': 'integer', 'id': '11535264367', 'key': 'z'}], + 'id': '11567102053', + 'key': 'feat2_with_var', + }, + ], + 'experiments': [ + { + 'status': 'Running', + 'key': 'feat_with_var_test', + 'layerId': '11504144555', + 'trafficAllocation': [{'entityId': '11617170975', 'endOfRange': 10000}], + 'audienceIds': [ + '3468206642', + '3988293898', + '3988293899', + '3468206646', + '3468206647', + '3468206644', + '3468206643', + ], + 'variations': [ + { + 'variables': [{'id': '11535264366', 'value': 'xyz'}], + 'id': '11617170975', + 'key': 'variation_2', + 'featureEnabled': True, + } + ], + 'forcedVariations': {}, + 'id': '11564051718', + }, + { + 'id': '1323241597', + 'key': 'typed_audience_experiment', + 'layerId': '1630555627', + 'status': 'Running', + 'variations': [{'id': '1423767503', 'key': 'A', 'variables': []}], + 'trafficAllocation': [{'entityId': '1423767503', 'endOfRange': 10000}], + 'audienceIds': [ + '3468206642', + '3988293898', + '3988293899', + '3468206646', + '3468206647', + '3468206644', + '3468206643', + ], + 'forcedVariations': {}, + }, + { + 'id': '1323241598', + 'key': 'audience_combinations_experiment', + 'layerId': '1323241598', + 'status': 'Running', + 'variations': [{'id': '1423767504', 'key': 'A', 'variables': []}], + 'trafficAllocation': [{'entityId': '1423767504', 'endOfRange': 10000}], + 'audienceIds': ['0'], + 'audienceConditions': [ + 'and', + ['or', '3468206642', '3988293898'], + ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643'], + ], + 'forcedVariations': {}, + }, + { + 'id': '1323241599', + 'key': 'feat2_with_var_test', + 'layerId': '1323241600', + 'status': 'Running', + 'variations': [ + { + 'variables': [{'id': '11535264367', 'value': '150'}], + 'id': '1423767505', + 'key': 'variation_2', + 'featureEnabled': True, + } + ], + 'trafficAllocation': [{'entityId': '1423767505', 'endOfRange': 10000}], + 'audienceIds': ['0'], + 'audienceConditions': [ + 'and', + ['or', '3468206642', '3988293898'], + ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643'], + ], + 'forcedVariations': {}, + }, + ], + 'audiences': [ + { + 'id': '3468206642', + 'name': 'exactString', + 'conditions': '["and", ["or", ["or", {"name": "house", ' + '"type": "custom_attribute", "value": "Gryffindor"}]]]', + }, + { + 'id': '3988293898', + 'name': '$$dummySubstringString', + 'conditions': '{ "type": "custom_attribute", ' + '"name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + { + 'id': '3988293899', + 'name': '$$dummyExists', + 'conditions': '{ "type": "custom_attribute", ' + '"name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + { + 'id': '3468206646', + 'name': '$$dummyExactNumber', + 'conditions': '{ "type": "custom_attribute", ' + '"name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + { + 'id': '3468206647', + 'name': '$$dummyGtNumber', + 'conditions': '{ "type": "custom_attribute", ' + '"name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + { + 'id': '3468206644', + 'name': '$$dummyLtNumber', + 'conditions': '{ "type": "custom_attribute", ' + '"name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + { + 'id': '3468206643', + 'name': '$$dummyExactBoolean', + 'conditions': '{ "type": "custom_attribute", ' + '"name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + { + 'id': '3468206645', + 'name': '$$dummyMultipleCustomAttrs', + 'conditions': '{ "type": "custom_attribute", ' + '"name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + { + 'id': '0', + 'name': '$$dummy', + 'conditions': '{ "type": "custom_attribute", ' + '"name": "$opt_dummy_attribute", "value": "impossible_value" }', + }, + ], + 'typedAudiences': [ + { + 'id': '3988293898', + 'name': 'substringString', + 'conditions': [ + 'and', + [ + 'or', + [ + 'or', + { + 'name': 'house', + 'type': 'custom_attribute', + 'match': 'substring', + 'value': 'Slytherin', + }, + ], + ], + ], + }, + { + 'id': '3988293899', + 'name': 'exists', + 'conditions': [ + 'and', + [ + 'or', + ['or', {'name': 'favorite_ice_cream', 'type': 'custom_attribute', 'match': 'exists'}], + ], + ], + }, + { + 'id': '3468206646', + 'name': 'exactNumber', + 'conditions': [ + 'and', + [ + 'or', + ['or', {'name': 'lasers', 'type': 'custom_attribute', 'match': 'exact', 'value': 45.5}], + ], + ], + }, + { + 'id': '3468206647', + 'name': 'gtNumber', + 'conditions': [ + 'and', + ['or', ['or', {'name': 'lasers', 'type': 'custom_attribute', 'match': 'gt', 'value': 70}]], + ], + }, + { + 'id': '3468206644', + 'name': 'ltNumber', + 'conditions': [ + 'and', + ['or', ['or', {'name': 'lasers', 'type': 'custom_attribute', 'match': 'lt', 'value': 1.0}]], + ], + }, + { + 'id': '3468206643', + 'name': 'exactBoolean', + 'conditions': [ + 'and', + [ + 'or', + [ + 'or', + {'name': 'should_do_it', 'type': 'custom_attribute', 'match': 'exact', 'value': True}, + ], + ], + ], + }, + { + 'id': '3468206645', + 'name': 'multiple_custom_attrs', + 'conditions': [ + "and", + [ + "or", + [ + "or", + {"type": "custom_attribute", "name": "browser", "value": "chrome"}, + {"type": "custom_attribute", "name": "browser", "value": "firefox"}, + ], + ], + ], + }, + ], + 'groups': [], + 'attributes': [ + {'key': 'house', 'id': '594015'}, + {'key': 'lasers', 'id': '594016'}, + {'key': 'should_do_it', 'id': '594017'}, + {'key': 'favorite_ice_cream', 'id': '594018'}, + ], + 'botFiltering': False, + 'accountId': '4879520872', + 'events': [ + {'key': 'item_bought', 'id': '594089', 'experimentIds': ['11564051718', '1323241597']}, + {'key': 'user_signed_up', 'id': '594090', 'experimentIds': ['1323241598', '1323241599']}, + ], + 'revision': '3', } - ], - 'revision': '3' - } - config = getattr(self, config_dict) - self.optimizely = optimizely.Optimizely(json.dumps(config)) - self.project_config = self.optimizely.config_manager.get_config() + config = getattr(self, config_dict) + self.optimizely = optimizely.Optimizely(json.dumps(config)) + self.project_config = self.optimizely.config_manager.get_config() diff --git a/tests/benchmarking/benchmarking_tests.py b/tests/benchmarking/benchmarking_tests.py index cbd8f5cb..c8f86caf 100644 --- a/tests/benchmarking/benchmarking_tests.py +++ b/tests/benchmarking/benchmarking_tests.py @@ -24,144 +24,149 @@ class BenchmarkingTests(object): - - def create_object(self, datafile): - start_time = time.clock() - optimizely.Optimizely(json.dumps(datafile)) - end_time = time.clock() - return (end_time - start_time) - - def create_object_schema_validation_off(self, datafile): - start_time = time.clock() - optimizely.Optimizely(json.dumps(datafile), skip_json_validation=True) - end_time = time.clock() - return (end_time - start_time) - - def activate_with_no_attributes(self, optimizely_obj, user_id): - start_time = time.clock() - variation_key = optimizely_obj.activate('testExperiment2', user_id) - end_time = time.clock() - assert variation_key == 'control' - return (end_time - start_time) - - def activate_with_attributes(self, optimizely_obj, user_id): - start_time = time.clock() - variation_key = optimizely_obj.activate('testExperimentWithFirefoxAudience', - user_id, attributes={'browser_type': 'firefox'}) - end_time = time.clock() - assert variation_key == 'variation' - return (end_time - start_time) - - def activate_with_forced_variation(self, optimizely_obj, user_id): - start_time = time.clock() - variation_key = optimizely_obj.activate('testExperiment2', user_id) - end_time = time.clock() - assert variation_key == 'variation' - return (end_time - start_time) - - def activate_grouped_experiment_no_attributes(self, optimizely_obj, user_id): - start_time = time.clock() - variation_key = optimizely_obj.activate('mutex_exp2', user_id) - end_time = time.clock() - assert variation_key == 'b' - return (end_time - start_time) - - def activate_grouped_experiment_with_attributes(self, optimizely_obj, user_id): - start_time = time.clock() - variation_key = optimizely_obj.activate('mutex_exp1', user_id, attributes={'browser_type': 'chrome'}) - end_time = time.clock() - assert variation_key == 'a' - return (end_time - start_time) - - def get_variation_with_no_attributes(self, optimizely_obj, user_id): - start_time = time.clock() - variation_key = optimizely_obj.get_variation('testExperiment2', user_id) - end_time = time.clock() - assert variation_key == 'control' - return (end_time - start_time) - - def get_variation_with_attributes(self, optimizely_obj, user_id): - start_time = time.clock() - variation_key = optimizely_obj.get_variation('testExperimentWithFirefoxAudience', - user_id, attributes={'browser_type': 'firefox'}) - end_time = time.clock() - assert variation_key == 'variation' - return (end_time - start_time) - - def get_variation_with_forced_variation(self, optimizely_obj, user_id): - start_time = time.clock() - variation_key = optimizely_obj.get_variation('testExperiment2', user_id) - end_time = time.clock() - assert variation_key == 'variation' - return (end_time - start_time) - - def get_variation_grouped_experiment_no_attributes(self, optimizely_obj, user_id): - start_time = time.clock() - variation_key = optimizely_obj.get_variation('mutex_exp2', user_id) - end_time = time.clock() - assert variation_key == 'b' - return (end_time - start_time) - - def get_variation_grouped_experiment_with_attributes(self, optimizely_obj, user_id): - start_time = time.clock() - variation_key = optimizely_obj.get_variation('mutex_exp1', user_id, attributes={'browser_type': 'chrome'}) - end_time = time.clock() - assert variation_key == 'a' - return (end_time - start_time) - - def track_with_attributes(self, optimizely_obj, user_id): - start_time = time.clock() - optimizely_obj.track('testEventWithAudiences', user_id, attributes={'browser_type': 'firefox'}) - end_time = time.clock() - return (end_time - start_time) - - def track_with_revenue(self, optimizely_obj, user_id): - start_time = time.clock() - optimizely_obj.track('testEvent', user_id, event_value=666) - end_time = time.clock() - return (end_time - start_time) - - def track_with_attributes_and_revenue(self, optimizely_obj, user_id): - start_time = time.clock() - optimizely_obj.track('testEventWithAudiences', user_id, - attributes={'browser_type': 'firefox'}, event_value=666) - end_time = time.clock() - return (end_time - start_time) - - def track_no_attributes_no_revenue(self, optimizely_obj, user_id): - start_time = time.clock() - optimizely_obj.track('testEvent', user_id) - end_time = time.clock() - return (end_time - start_time) - - def track_grouped_experiment(self, optimizely_obj, user_id): - start_time = time.clock() - optimizely_obj.track('testEventWithMultipleGroupedExperiments', user_id) - end_time = time.clock() - return (end_time - start_time) - - def track_grouped_experiment_with_attributes(self, optimizely_obj, user_id): - start_time = time.clock() - optimizely_obj.track('testEventWithMultipleExperiments', user_id, attributes={'browser_type': 'chrome'}) - end_time = time.clock() - return (end_time - start_time) - - def track_grouped_experiment_with_revenue(self, optimizely_obj, user_id): - start_time = time.clock() - optimizely_obj.track('testEventWithMultipleGroupedExperiments', user_id, event_value=666) - end_time = time.clock() - return (end_time - start_time) - - def track_grouped_experiment_with_attributes_and_revenue(self, optimizely_obj, user_id): - start_time = time.clock() - optimizely_obj.track('testEventWithMultipleExperiments', user_id, - attributes={'browser_type': 'chrome'}, event_value=666) - end_time = time.clock() - return (end_time - start_time) + def create_object(self, datafile): + start_time = time.clock() + optimizely.Optimizely(json.dumps(datafile)) + end_time = time.clock() + return end_time - start_time + + def create_object_schema_validation_off(self, datafile): + start_time = time.clock() + optimizely.Optimizely(json.dumps(datafile), skip_json_validation=True) + end_time = time.clock() + return end_time - start_time + + def activate_with_no_attributes(self, optimizely_obj, user_id): + start_time = time.clock() + variation_key = optimizely_obj.activate('testExperiment2', user_id) + end_time = time.clock() + assert variation_key == 'control' + return end_time - start_time + + def activate_with_attributes(self, optimizely_obj, user_id): + start_time = time.clock() + variation_key = optimizely_obj.activate( + 'testExperimentWithFirefoxAudience', user_id, attributes={'browser_type': 'firefox'}, + ) + end_time = time.clock() + assert variation_key == 'variation' + return end_time - start_time + + def activate_with_forced_variation(self, optimizely_obj, user_id): + start_time = time.clock() + variation_key = optimizely_obj.activate('testExperiment2', user_id) + end_time = time.clock() + assert variation_key == 'variation' + return end_time - start_time + + def activate_grouped_experiment_no_attributes(self, optimizely_obj, user_id): + start_time = time.clock() + variation_key = optimizely_obj.activate('mutex_exp2', user_id) + end_time = time.clock() + assert variation_key == 'b' + return end_time - start_time + + def activate_grouped_experiment_with_attributes(self, optimizely_obj, user_id): + start_time = time.clock() + variation_key = optimizely_obj.activate('mutex_exp1', user_id, attributes={'browser_type': 'chrome'}) + end_time = time.clock() + assert variation_key == 'a' + return end_time - start_time + + def get_variation_with_no_attributes(self, optimizely_obj, user_id): + start_time = time.clock() + variation_key = optimizely_obj.get_variation('testExperiment2', user_id) + end_time = time.clock() + assert variation_key == 'control' + return end_time - start_time + + def get_variation_with_attributes(self, optimizely_obj, user_id): + start_time = time.clock() + variation_key = optimizely_obj.get_variation( + 'testExperimentWithFirefoxAudience', user_id, attributes={'browser_type': 'firefox'}, + ) + end_time = time.clock() + assert variation_key == 'variation' + return end_time - start_time + + def get_variation_with_forced_variation(self, optimizely_obj, user_id): + start_time = time.clock() + variation_key = optimizely_obj.get_variation('testExperiment2', user_id) + end_time = time.clock() + assert variation_key == 'variation' + return end_time - start_time + + def get_variation_grouped_experiment_no_attributes(self, optimizely_obj, user_id): + start_time = time.clock() + variation_key = optimizely_obj.get_variation('mutex_exp2', user_id) + end_time = time.clock() + assert variation_key == 'b' + return end_time - start_time + + def get_variation_grouped_experiment_with_attributes(self, optimizely_obj, user_id): + start_time = time.clock() + variation_key = optimizely_obj.get_variation('mutex_exp1', user_id, attributes={'browser_type': 'chrome'}) + end_time = time.clock() + assert variation_key == 'a' + return end_time - start_time + + def track_with_attributes(self, optimizely_obj, user_id): + start_time = time.clock() + optimizely_obj.track('testEventWithAudiences', user_id, attributes={'browser_type': 'firefox'}) + end_time = time.clock() + return end_time - start_time + + def track_with_revenue(self, optimizely_obj, user_id): + start_time = time.clock() + optimizely_obj.track('testEvent', user_id, event_value=666) + end_time = time.clock() + return end_time - start_time + + def track_with_attributes_and_revenue(self, optimizely_obj, user_id): + start_time = time.clock() + optimizely_obj.track( + 'testEventWithAudiences', user_id, attributes={'browser_type': 'firefox'}, event_value=666, + ) + end_time = time.clock() + return end_time - start_time + + def track_no_attributes_no_revenue(self, optimizely_obj, user_id): + start_time = time.clock() + optimizely_obj.track('testEvent', user_id) + end_time = time.clock() + return end_time - start_time + + def track_grouped_experiment(self, optimizely_obj, user_id): + start_time = time.clock() + optimizely_obj.track('testEventWithMultipleGroupedExperiments', user_id) + end_time = time.clock() + return end_time - start_time + + def track_grouped_experiment_with_attributes(self, optimizely_obj, user_id): + start_time = time.clock() + optimizely_obj.track( + 'testEventWithMultipleExperiments', user_id, attributes={'browser_type': 'chrome'}, + ) + end_time = time.clock() + return end_time - start_time + + def track_grouped_experiment_with_revenue(self, optimizely_obj, user_id): + start_time = time.clock() + optimizely_obj.track('testEventWithMultipleGroupedExperiments', user_id, event_value=666) + end_time = time.clock() + return end_time - start_time + + def track_grouped_experiment_with_attributes_and_revenue(self, optimizely_obj, user_id): + start_time = time.clock() + optimizely_obj.track( + 'testEventWithMultipleExperiments', user_id, attributes={'browser_type': 'chrome'}, event_value=666, + ) + end_time = time.clock() + return end_time - start_time def compute_average(values): - """ Given a set of values compute the average. + """ Given a set of values compute the average. Args: values: Set of values for which average is to be computed. @@ -169,11 +174,11 @@ def compute_average(values): Returns: Average of all values. """ - return float(sum(values)) / len(values) + return float(sum(values)) / len(values) def compute_median(values): - """ Given a set of values compute the median. + """ Given a set of values compute the median. Args: values: Set of values for which median is to be computed. @@ -182,55 +187,62 @@ def compute_median(values): Median of all values. """ - sorted_values = sorted(values) - num1 = (len(values) - 1) / 2 - num2 = len(values) / 2 - return float(sorted_values[num1] + sorted_values[num2]) / 2 + sorted_values = sorted(values) + num1 = (len(values) - 1) / 2 + num2 = len(values) / 2 + return float(sorted_values[num1] + sorted_values[num2]) / 2 def display_results(results_average, results_median): - """ Format and print results on screen. + """ Format and print results on screen. Args: results_average: Dict holding averages. results_median: Dict holding medians. """ - table_data = [] - table_headers = ['Test Name', - '10 Experiment Average', '10 Experiment Median', - '25 Experiment Average', '25 Experiment Median', - '50 Experiment Average', '50 Experiment Median'] - for test_name, test_method in BenchmarkingTests.__dict__.iteritems(): - if callable(test_method): - row_data = [test_name] - for experiment_count in sorted(data.datafiles.keys()): - row_data.append(results_average.get(experiment_count).get(test_name)) - row_data.append(results_median.get(experiment_count).get(test_name)) - table_data.append(row_data) + table_data = [] + table_headers = [ + 'Test Name', + '10 Experiment Average', + '10 Experiment Median', + '25 Experiment Average', + '25 Experiment Median', + '50 Experiment Average', + '50 Experiment Median', + ] + for test_name, test_method in BenchmarkingTests.__dict__.iteritems(): + if callable(test_method): + row_data = [test_name] + for experiment_count in sorted(data.datafiles.keys()): + row_data.append(results_average.get(experiment_count).get(test_name)) + row_data.append(results_median.get(experiment_count).get(test_name)) + table_data.append(row_data) - print tabulate(table_data, headers=table_headers) + print tabulate(table_data, headers=table_headers) def run_benchmarking_tests(): - all_test_results_average = {} - all_test_results_median = {} - test_data = data.test_data - for experiment_count in data.datafiles: - all_test_results_average[experiment_count] = {} - all_test_results_median[experiment_count] = {} - for test_name, test_method in BenchmarkingTests.__dict__.iteritems(): - if callable(test_method): - values = [] - for i in xrange(ITERATIONS): - values.append(1000 * test_method(BenchmarkingTests(), *test_data.get(test_name).get(experiment_count))) - time_in_milliseconds_avg = compute_average(values) - time_in_milliseconds_median = compute_median(values) - all_test_results_average[experiment_count][test_name] = time_in_milliseconds_avg - all_test_results_median[experiment_count][test_name] = time_in_milliseconds_median - - display_results(all_test_results_average, all_test_results_median) + all_test_results_average = {} + all_test_results_median = {} + test_data = data.test_data + for experiment_count in data.datafiles: + all_test_results_average[experiment_count] = {} + all_test_results_median[experiment_count] = {} + for test_name, test_method in BenchmarkingTests.__dict__.iteritems(): + if callable(test_method): + values = [] + for i in xrange(ITERATIONS): + values.append( + 1000 * test_method(BenchmarkingTests(), *test_data.get(test_name).get(experiment_count)) + ) + time_in_milliseconds_avg = compute_average(values) + time_in_milliseconds_median = compute_median(values) + all_test_results_average[experiment_count][test_name] = time_in_milliseconds_avg + all_test_results_median[experiment_count][test_name] = time_in_milliseconds_median + + display_results(all_test_results_average, all_test_results_median) if __name__ == '__main__': - run_benchmarking_tests() + run_benchmarking_tests() diff --git a/tests/benchmarking/data.py b/tests/benchmarking/data.py index ae44146e..edaaf740 100644 --- a/tests/benchmarking/data.py +++ b/tests/benchmarking/data.py @@ -17,3269 +17,1478 @@ config_10_exp = { - "experiments": [ - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment4", - "trafficAllocation": [ - { - "entityId": "6373141147", - "endOfRange": 5000 - }, - { - "entityId": "6373141148", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6373141147", - "key": "control" - }, - { - "id": "6373141148", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6358043286" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment5", - "trafficAllocation": [ - { - "entityId": "6335242053", - "endOfRange": 5000 - }, - { - "entityId": "6335242054", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6335242053", - "key": "control" - }, - { - "id": "6335242054", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6364835526" - }, - { - "status": "Paused", - "percentageIncluded": 10000, - "key": "testExperimentNotRunning", - "trafficAllocation": [ - { - "entityId": "6377281127", - "endOfRange": 5000 - }, - { - "entityId": "6377281128", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6377281127", - "key": "control" - }, - { - "id": "6377281128", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6367444440" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment1", - "trafficAllocation": [ - { - "entityId": "6384330451", - "endOfRange": 5000 - }, - { - "entityId": "6384330452", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6384330451", - "key": "control" - }, - { - "id": "6384330452", - "key": "variation" - } - ], - "forcedVariations": { - "variation_user": "variation", - "control_user": "control" - }, - "id": "6367863211" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment3", - "trafficAllocation": [ - { - "entityId": "6376141758", - "endOfRange": 5000 - }, - { - "entityId": "6376141759", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6376141758", - "key": "control" - }, - { - "id": "6376141759", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6370392407" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment6", - "trafficAllocation": [ - { - "entityId": "6379060914", - "endOfRange": 5000 - }, - { - "entityId": "6379060915", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6379060914", - "key": "control" - }, - { - "id": "6379060915", - "key": "variation" - } - ], - "forcedVariations": { - "forced_variation_user": "variation" - }, - "id": "6370821515" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment2", - "trafficAllocation": [ - { - "entityId": "6386700062", - "endOfRange": 5000 - }, - { - "entityId": "6386700063", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6386700062", - "key": "control" - }, - { - "id": "6386700063", - "key": "variation" - } - ], - "forcedVariations": { - "variation_user": "variation", - "control_user": "control" - }, - "id": "6376870125" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperimentWithFirefoxAudience", - "trafficAllocation": [ - { - "entityId": "6333082303", - "endOfRange": 5000 - }, - { - "entityId": "6333082304", - "endOfRange": 10000 - } - ], - "audienceIds": [ - "6369992312" - ], - "variations": [ - { - "id": "6333082303", - "key": "control" - }, - { - "id": "6333082304", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6383811281" - } - ], - "version": "1", - "audiences": [ - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"safari\"}]]]", - "id": "6352892614", - "name": "Safari users" - }, - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"android\"}]]]", - "id": "6355234780", - "name": "Android users" - }, - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"desktop\"}]]]", - "id": "6360574256", - "name": "Desktop users" - }, - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"opera\"}]]]", - "id": "6365864533", - "name": "Opera users" - }, - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"tablet\"}]]]", - "id": "6369831151", - "name": "Tablet users" - }, - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"firefox\"}]]]", - "id": "6369992312", - "name": "Firefox users" - }, - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"chrome\"}]]]", - "id": "6373141157", - "name": "Chrome users" - }, - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"ie\"}]]]", - "id": "6378191386", - "name": "IE users" - } - ], - "dimensions": [ - { - "id": "6359881003", - "key": "browser_type", - "segmentId": "6380740826" - } - ], - "groups": [ - { - "policy": "random", - "trafficAllocation": [ - - ], - "experiments": [ - - ], - "id": "6367902163" - }, - { - "policy": "random", - "trafficAllocation": [ - - ], - "experiments": [ - - ], - "id": "6393150032" - }, - { - "policy": "random", - "trafficAllocation": [ - { - "entityId": "6450630664", - "endOfRange": 5000 - }, - { - "entityId": "6447021179", - "endOfRange": 10000 - } - ], - "experiments": [ - { - "status": "Running", - "percentageIncluded": 5000, - "key": "mutex_exp2", - "trafficAllocation": [ - { - "entityId": "6453410972", - "endOfRange": 5000 - }, - { - "entityId": "6453410973", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6453410972", - "key": "a" - }, - { - "id": "6453410973", - "key": "b" - } - ], - "forcedVariations": { - "user_b": "b", - "user_a": "a" - }, - "id": "6447021179" - }, - { - "status": "Running", - "percentageIncluded": 5000, - "key": "mutex_exp1", - "trafficAllocation": [ - { - "entityId": "6451680205", - "endOfRange": 5000 - }, - { - "entityId": "6451680206", - "endOfRange": 10000 - } - ], - "audienceIds": [ - "6373141157" - ], - "variations": [ - { - "id": "6451680205", - "key": "a" - }, - { - "id": "6451680206", - "key": "b" - } - ], - "forcedVariations": { - - }, - "id": "6450630664" - } - ], - "id": "6436903041" - } - ], - "projectId": "6377970066", - "accountId": "6365361536", - "events": [ - { - "experimentIds": [ - "6450630664", - "6447021179" - ], - "id": "6370392432", - "key": "testEventWithMultipleGroupedExperiments" - }, - { - "experimentIds": [ - "6367863211" - ], - "id": "6372590948", - "key": "testEvent" - }, - { - "experimentIds": [ - "6364835526", - "6450630664", - "6367863211", - "6376870125", - "6383811281", - "6358043286", - "6370392407", - "6367444440", - "6370821515", - "6447021179" - ], - "id": "6372952486", - "key": "testEventWithMultipleExperiments" - }, - { - "experimentIds": [ - "6367444440" - ], - "id": "6380961307", - "key": "testEventWithExperimentNotRunning" - }, - { - "experimentIds": [ - "6383811281" - ], - "id": "6384781388", - "key": "testEventWithAudiences" - }, - { - "experimentIds": [ - - ], - "id": "6386521015", - "key": "testEventWithoutExperiments" - }, - { - "experimentIds": [ - "6450630664", - "6383811281", - "6376870125" - ], - "id": "6316734272", - "key": "Total Revenue" - } - ], - "revision": "83" -} - -config_25_exp = { - "experiments": [ - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment12", - "trafficAllocation": [ - { - "entityId": "6387320950", - "endOfRange": 5000 - }, - { - "entityId": "6387320951", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6387320950", - "key": "control" - }, - { - "id": "6387320951", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6344617435" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment19", - "trafficAllocation": [ - { - "entityId": "6380932289", - "endOfRange": 5000 - }, - { - "entityId": "6380932290", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6380932289", - "key": "control" - }, - { - "id": "6380932290", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6349682899" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment21", - "trafficAllocation": [ - { - "entityId": "6356833706", - "endOfRange": 5000 - }, - { - "entityId": "6356833707", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6356833706", - "key": "control" - }, - { - "id": "6356833707", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6350472041" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment7", - "trafficAllocation": [ - { - "entityId": "6367863508", - "endOfRange": 5000 - }, - { - "entityId": "6367863509", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6367863508", - "key": "control" - }, - { - "id": "6367863509", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6352512126" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment15", - "trafficAllocation": [ - { - "entityId": "6379652128", - "endOfRange": 5000 - }, - { - "entityId": "6379652129", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6379652128", - "key": "control" - }, - { - "id": "6379652129", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6357622647" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment16", - "trafficAllocation": [ - { - "entityId": "6359551503", - "endOfRange": 5000 - }, - { - "entityId": "6359551504", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6359551503", - "key": "control" - }, - { - "id": "6359551504", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6361100609" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment8", - "trafficAllocation": [ - { - "entityId": "6378191496", - "endOfRange": 5000 - }, - { - "entityId": "6378191497", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6378191496", - "key": "control" - }, - { - "id": "6378191497", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6361743021" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperimentWithFirefoxAudience", - "trafficAllocation": [ - { - "entityId": "6380932291", - "endOfRange": 5000 - }, - { - "entityId": "6380932292", - "endOfRange": 10000 - } - ], - "audienceIds": [ - "6317864099" - ], - "variations": [ - { - "id": "6380932291", - "key": "control" - }, - { - "id": "6380932292", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6361931183" - }, - { - "status": "Not started", - "percentageIncluded": 10000, - "key": "testExperimentNotRunning", - "trafficAllocation": [ - { - "entityId": "6377723538", - "endOfRange": 5000 - }, - { - "entityId": "6377723539", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6377723538", - "key": "control" - }, - { - "id": "6377723539", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6362042330" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment5", - "trafficAllocation": [ - { - "entityId": "6361100607", - "endOfRange": 5000 - }, - { - "entityId": "6361100608", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6361100607", - "key": "control" - }, - { - "id": "6361100608", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6365780767" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment0", - "trafficAllocation": [ - { - "entityId": "6379122883", - "endOfRange": 5000 - }, - { - "entityId": "6379122884", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6379122883", - "key": "control" - }, - { - "id": "6379122884", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6366023085" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment2", - "trafficAllocation": [ - { - "entityId": "6373980983", - "endOfRange": 5000 - }, - { - "entityId": "6373980984", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6373980983", - "key": "control" - }, - { - "id": "6373980984", - "key": "variation" - } - ], - "forcedVariations": { - "variation_user": "variation", - "control_user": "control" - }, - "id": "6367473060" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment13", - "trafficAllocation": [ - { - "entityId": "6361931181", - "endOfRange": 5000 - }, - { - "entityId": "6361931182", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6361931181", - "key": "control" - }, - { - "id": "6361931182", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6367842673" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment18", - "trafficAllocation": [ - { - "entityId": "6375121958", - "endOfRange": 5000 - }, - { - "entityId": "6375121959", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6375121958", - "key": "control" - }, - { - "id": "6375121959", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6367902537" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment17", - "trafficAllocation": [ - { - "entityId": "6353582033", - "endOfRange": 5000 - }, - { - "entityId": "6353582034", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6353582033", - "key": "control" - }, - { - "id": "6353582034", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6368671885" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment11", - "trafficAllocation": [ - { - "entityId": "6355235088", - "endOfRange": 5000 - }, - { - "entityId": "6355235089", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6355235088", - "key": "control" - }, - { - "id": "6355235089", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6369512098" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment3", - "trafficAllocation": [ - { - "entityId": "6355235086", - "endOfRange": 5000 - }, - { - "entityId": "6355235087", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6355235086", - "key": "control" - }, - { - "id": "6355235087", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6371041921" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment10", - "trafficAllocation": [ - { - "entityId": "6382231014", - "endOfRange": 5000 - }, - { - "entityId": "6382231015", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6382231014", - "key": "control" - }, - { - "id": "6382231015", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6375231186" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment20", - "trafficAllocation": [ - { - "entityId": "6362951972", - "endOfRange": 5000 - }, - { - "entityId": "6362951973", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6362951972", - "key": "control" - }, - { - "id": "6362951973", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6377131549" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment9", - "trafficAllocation": [ - { - "entityId": "6369462637", - "endOfRange": 5000 - }, - { - "entityId": "6369462638", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6369462637", - "key": "control" - }, - { - "id": "6369462638", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6382251626" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment14", - "trafficAllocation": [ - { - "entityId": "6388520034", - "endOfRange": 5000 - }, - { - "entityId": "6388520035", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6388520034", - "key": "control" - }, - { - "id": "6388520035", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6383770101" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment6", - "trafficAllocation": [ - { - "entityId": "6378802069", - "endOfRange": 5000 - }, - { - "entityId": "6378802070", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6378802069", - "key": "control" - }, - { - "id": "6378802070", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6386411740" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment4", - "trafficAllocation": [ - { - "entityId": "6350263010", - "endOfRange": 5000 - }, - { - "entityId": "6350263011", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6350263010", - "key": "control" - }, - { - "id": "6350263011", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6386460951" - } - ], - "version": "1", - "audiences": [ - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"firefox\"}]]]", - "id": "6317864099", - "name": "Firefox users" - }, - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"safari\"}]]]", - "id": "6360592016", - "name": "Safari users" - }, - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"chrome\"}]]]", - "id": "6361743063", - "name": "Chrome users" - }, - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"desktop\"}]]]", - "id": "6372190788", - "name": "Desktop users" - }, - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"android\"}]]]", - "id": "6376141951", - "name": "Android users" - }, - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"ie\"}]]]", - "id": "6377605300", - "name": "IE users" - }, - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"tablet\"}]]]", - "id": "6378191534", - "name": "Tablet users" - }, - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"opera\"}]]]", - "id": "6386521201", - "name": "Opera users" - } - ], - "dimensions": [ - { - "id": "6381732124", - "key": "browser_type", - "segmentId": "6388221232" - } - ], - "groups": [ - { - "policy": "random", - "trafficAllocation": [ - { - "entityId": "6416416234", - "endOfRange": 5000 - }, - { - "entityId": "6451651052", - "endOfRange": 10000 - } - ], - "experiments": [ - { - "status": "Running", - "percentageIncluded": 5000, - "key": "mutex_exp1", - "trafficAllocation": [ - { - "entityId": "6448110056", - "endOfRange": 5000 - }, - { - "entityId": "6448110057", - "endOfRange": 10000 - } - ], - "audienceIds": [ - "6361743063" - ], - "variations": [ - { - "id": "6448110056", - "key": "a" - }, - { - "id": "6448110057", - "key": "b" - } - ], - "forcedVariations": { - - }, - "id": "6416416234" - }, - { - "status": "Running", - "percentageIncluded": 5000, - "key": "mutex_exp2", - "trafficAllocation": [ - { - "entityId": "6437485007", - "endOfRange": 5000 - }, - { - "entityId": "6437485008", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6437485007", - "key": "a" - }, - { - "id": "6437485008", - "key": "b" - } - ], - "forcedVariations": { - "user_b": "b", - "user_a": "a" - }, - "id": "6451651052" - } - ], - "id": "6441101079" - } - ], - "projectId": "6379191198", - "accountId": "6365361536", - "events": [ - { - "experimentIds": [ - - ], - "id": "6360377431", - "key": "testEventWithoutExperiments" - }, - { - "experimentIds": [ - "6366023085" - ], - "id": "6373184839", - "key": "testEvent" - }, - { - "experimentIds": [ - "6451651052" - ], - "id": "6379061102", - "key": "testEventWithMultipleGroupedExperiments" - }, - { - "experimentIds": [ - "6362042330" - ], - "id": "6385201698", - "key": "testEventWithExperimentNotRunning" - }, - { - "experimentIds": [ - "6361931183" - ], - "id": "6385551103", - "key": "testEventWithAudiences" - }, - { - "experimentIds": [ - "6371041921", - "6382251626", - "6368671885", - "6361743021", - "6386460951", - "6377131549", - "6365780767", - "6369512098", - "6367473060", - "6366023085", - "6361931183", - "6361100609", - "6367902537", - "6375231186", - "6349682899", - "6362042330", - "6344617435", - "6386411740", - "6350472041", - "6416416234", - "6451651052", - "6367842673", - "6383770101", - "6357622647", - "6352512126" - ], - "id": "6386470923", - "key": "testEventWithMultipleExperiments" - }, - { - "experimentIds": [ - "6361931183", - "6416416234", - "6367473060" - ], - "id": "6386460946", - "key": "Total Revenue" - } - ], - "revision": "92" -} - -config_50_exp = { - "experiments": [ - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment31", - "trafficAllocation": [ - { - "entityId": "6383523065", - "endOfRange": 5000 - }, - { - "entityId": "6383523066", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6383523065", - "key": "control" - }, - { - "id": "6383523066", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6313973431" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment15", - "trafficAllocation": [ - { - "entityId": "6363413697", - "endOfRange": 5000 - }, - { - "entityId": "6363413698", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6363413697", - "key": "control" - }, - { - "id": "6363413698", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6332666164" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment33", - "trafficAllocation": [ - { - "entityId": "6330789404", - "endOfRange": 5000 - }, - { - "entityId": "6330789405", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6330789404", - "key": "control" - }, - { - "id": "6330789405", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6338678718" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment38", - "trafficAllocation": [ - { - "entityId": "6376706101", - "endOfRange": 5000 - }, - { - "entityId": "6376706102", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6376706101", - "key": "control" - }, - { - "id": "6376706102", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6338678719" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment44", - "trafficAllocation": [ - { - "entityId": "6316734590", - "endOfRange": 5000 - }, - { - "entityId": "6316734591", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6316734590", - "key": "control" - }, - { - "id": "6316734591", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6355784786" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperimentWithFirefoxAudience", - "trafficAllocation": [ - { - "entityId": "6362476365", - "endOfRange": 5000 - }, - { - "entityId": "6362476366", - "endOfRange": 10000 - } - ], - "audienceIds": [ - "6373742627" - ], - "variations": [ - { - "id": "6362476365", - "key": "control" - }, - { - "id": "6362476366", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6359356006" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment14", - "trafficAllocation": [ - { - "entityId": "6327476066", - "endOfRange": 5000 - }, - { - "entityId": "6327476067", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6327476066", - "key": "control" - }, - { - "id": "6327476067", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6360796560" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment46", - "trafficAllocation": [ - { - "entityId": "6357247500", - "endOfRange": 5000 - }, - { - "entityId": "6357247501", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6357247500", - "key": "control" - }, - { - "id": "6357247501", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6361359596" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment16", - "trafficAllocation": [ - { - "entityId": "6378191544", - "endOfRange": 5000 - }, - { - "entityId": "6378191545", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6378191544", - "key": "control" - }, - { - "id": "6378191545", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6361743077" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment10", - "trafficAllocation": [ - { - "entityId": "6372300744", - "endOfRange": 5000 - }, - { - "entityId": "6372300745", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6372300744", - "key": "control" - }, - { - "id": "6372300745", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6362476358" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment11", - "trafficAllocation": [ - { - "entityId": "6357247497", - "endOfRange": 5000 - }, - { - "entityId": "6357247498", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6357247497", - "key": "control" - }, - { - "id": "6357247498", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6362476359" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment12", - "trafficAllocation": [ - { - "entityId": "6368497829", - "endOfRange": 5000 - }, - { - "entityId": "6368497830", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6368497829", - "key": "control" - }, - { - "id": "6368497830", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6363607946" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment7", - "trafficAllocation": [ - { - "entityId": "6386590519", - "endOfRange": 5000 - }, - { - "entityId": "6386590520", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6386590519", - "key": "control" - }, - { - "id": "6386590520", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6364882055" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment6", - "trafficAllocation": [ - { - "entityId": "6385481560", - "endOfRange": 5000 - }, - { - "entityId": "6385481561", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6385481560", - "key": "control" - }, - { - "id": "6385481561", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6366023126" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment23", - "trafficAllocation": [ - { - "entityId": "6375122007", - "endOfRange": 5000 - }, - { - "entityId": "6375122008", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6375122007", - "key": "control" - }, - { - "id": "6375122008", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6367902584" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment13", - "trafficAllocation": [ - { - "entityId": "6360762679", - "endOfRange": 5000 - }, - { - "entityId": "6360762680", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6360762679", - "key": "control" - }, - { - "id": "6360762680", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6367922509" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment39", - "trafficAllocation": [ - { - "entityId": "6341311988", - "endOfRange": 5000 - }, - { - "entityId": "6341311989", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6341311988", - "key": "control" - }, - { - "id": "6341311989", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6369992702" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment4", - "trafficAllocation": [ - { - "entityId": "6370014876", - "endOfRange": 5000 - }, - { - "entityId": "6370014877", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6370014876", - "key": "control" - }, - { - "id": "6370014877", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6370815084" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment17", - "trafficAllocation": [ - { - "entityId": "6384651930", - "endOfRange": 5000 - }, - { - "entityId": "6384651931", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6384651930", - "key": "control" - }, - { - "id": "6384651931", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6371742027" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment42", - "trafficAllocation": [ - { - "entityId": "6371581616", - "endOfRange": 5000 - }, - { - "entityId": "6371581617", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6371581616", - "key": "control" - }, - { - "id": "6371581617", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6374064265" - }, - { - "status": "Not started", - "percentageIncluded": 10000, - "key": "testExperimentNotRunning", - "trafficAllocation": [ - { - "entityId": "6380740985", - "endOfRange": 5000 - }, - { - "entityId": "6380740986", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6380740985", - "key": "control" - }, - { - "id": "6380740986", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6375231238" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment36", - "trafficAllocation": [ - { - "entityId": "6380164945", - "endOfRange": 5000 - }, - { - "entityId": "6380164946", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6380164945", - "key": "control" - }, - { - "id": "6380164946", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6375494974" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment45", - "trafficAllocation": [ - { - "entityId": "6374765096", - "endOfRange": 5000 - }, - { - "entityId": "6374765097", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6374765096", - "key": "control" - }, - { - "id": "6374765097", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6375595048" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment43", - "trafficAllocation": [ - { - "entityId": "6385191624", - "endOfRange": 5000 - }, - { - "entityId": "6385191625", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6385191624", - "key": "control" - }, - { - "id": "6385191625", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6376141968" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment25", - "trafficAllocation": [ - { - "entityId": "6368955066", - "endOfRange": 5000 - }, - { - "entityId": "6368955067", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6368955066", - "key": "control" - }, - { - "id": "6368955067", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6376658685" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment2", - "trafficAllocation": [ - { - "entityId": "6382040994", - "endOfRange": 5000 - }, - { - "entityId": "6382040995", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6382040994", - "key": "control" - }, - { - "id": "6382040995", - "key": "variation" - } - ], - "forcedVariations": { - "variation_user": "variation", - "control_user": "control" - }, - "id": "6377001018" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment18", - "trafficAllocation": [ - { - "entityId": "6370582521", - "endOfRange": 5000 - }, - { - "entityId": "6370582522", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6370582521", - "key": "control" - }, - { - "id": "6370582522", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6377202148" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment24", - "trafficAllocation": [ - { - "entityId": "6381612278", - "endOfRange": 5000 - }, - { - "entityId": "6381612279", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6381612278", - "key": "control" - }, - { - "id": "6381612279", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6377723605" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment19", - "trafficAllocation": [ - { - "entityId": "6362476361", - "endOfRange": 5000 - }, - { - "entityId": "6362476362", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6362476361", - "key": "control" - }, - { - "id": "6362476362", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6379205044" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment20", - "trafficAllocation": [ - { - "entityId": "6370537428", - "endOfRange": 5000 - }, - { - "entityId": "6370537429", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6370537428", - "key": "control" - }, - { - "id": "6370537429", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6379205045" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment28", - "trafficAllocation": [ - { - "entityId": "6387291313", - "endOfRange": 5000 - }, - { - "entityId": "6387291314", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6387291313", - "key": "control" - }, - { - "id": "6387291314", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6379841378" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment35", - "trafficAllocation": [ - { - "entityId": "6375332081", - "endOfRange": 5000 - }, - { - "entityId": "6375332082", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6375332081", - "key": "control" - }, - { - "id": "6375332082", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6379900650" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment1", - "trafficAllocation": [ - { - "entityId": "6355235181", - "endOfRange": 5000 - }, - { - "entityId": "6355235182", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6355235181", - "key": "control" - }, - { - "id": "6355235182", - "key": "variation" - } - ], - "forcedVariations": { - "variation_user": "variation", - "control_user": "control" - }, - "id": "6380251600" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment8", - "trafficAllocation": [ - { - "entityId": "6310506102", - "endOfRange": 5000 - }, - { - "entityId": "6310506103", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6310506102", - "key": "control" - }, - { - "id": "6310506103", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6380932373" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment3", - "trafficAllocation": [ - { - "entityId": "6373612240", - "endOfRange": 5000 - }, - { - "entityId": "6373612241", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6373612240", - "key": "control" - }, - { - "id": "6373612241", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6380971484" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment22", - "trafficAllocation": [ - { - "entityId": "6360796561", - "endOfRange": 5000 - }, - { - "entityId": "6360796562", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6360796561", - "key": "control" - }, - { - "id": "6360796562", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6381631585" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment37", - "trafficAllocation": [ - { - "entityId": "6356824684", - "endOfRange": 5000 - }, - { - "entityId": "6356824685", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6356824684", - "key": "control" - }, - { - "id": "6356824685", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6381732143" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment41", - "trafficAllocation": [ - { - "entityId": "6389170550", - "endOfRange": 5000 - }, - { - "entityId": "6389170551", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6389170550", - "key": "control" - }, - { - "id": "6389170551", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6381781177" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment27", - "trafficAllocation": [ - { - "entityId": "6372591085", - "endOfRange": 5000 - }, - { - "entityId": "6372591086", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6372591085", - "key": "control" - }, - { - "id": "6372591086", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6382300680" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment26", - "trafficAllocation": [ - { - "entityId": "6375602097", - "endOfRange": 5000 - }, - { - "entityId": "6375602098", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6375602097", - "key": "control" - }, - { - "id": "6375602098", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6382682166" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment9", - "trafficAllocation": [ - { - "entityId": "6376221556", - "endOfRange": 5000 - }, - { - "entityId": "6376221557", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6376221556", - "key": "control" - }, - { - "id": "6376221557", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6382950966" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment29", - "trafficAllocation": [ - { - "entityId": "6382070548", - "endOfRange": 5000 - }, - { - "entityId": "6382070549", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6382070548", - "key": "control" - }, - { - "id": "6382070549", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6383120500" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment32", - "trafficAllocation": [ - { - "entityId": "6391210101", - "endOfRange": 5000 - }, - { - "entityId": "6391210102", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6391210101", - "key": "control" - }, - { - "id": "6391210102", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6383430268" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment30", - "trafficAllocation": [ - { - "entityId": "6364835927", - "endOfRange": 5000 - }, - { - "entityId": "6364835928", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6364835927", - "key": "control" - }, - { - "id": "6364835928", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6384711622" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment34", - "trafficAllocation": [ - { - "entityId": "6390151025", - "endOfRange": 5000 - }, - { - "entityId": "6390151026", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6390151025", - "key": "control" - }, - { - "id": "6390151026", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6384861073" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment21", - "trafficAllocation": [ - { - "entityId": "6384881124", - "endOfRange": 5000 - }, - { - "entityId": "6384881125", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6384881124", - "key": "control" - }, - { - "id": "6384881125", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6385551136" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment40", - "trafficAllocation": [ - { - "entityId": "6387261935", - "endOfRange": 5000 - }, - { - "entityId": "6387261936", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6387261935", - "key": "control" - }, - { - "id": "6387261936", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6387252155" - }, - { - "status": "Running", - "percentageIncluded": 10000, - "key": "testExperiment5", - "trafficAllocation": [ - { - "entityId": "6312093242", - "endOfRange": 5000 - }, - { - "entityId": "6312093243", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6312093242", - "key": "control" - }, - { - "id": "6312093243", - "key": "variation" - } - ], - "forcedVariations": { - - }, - "id": "6388170688" - } - ], - "version": "1", - "audiences": [ - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"android\"}]]]", - "id": "6366023138", - "name": "Android users" - }, - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"firefox\"}]]]", - "id": "6373742627", - "name": "Firefox users" - }, - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"ie\"}]]]", - "id": "6376161539", - "name": "IE users" - }, - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"desktop\"}]]]", - "id": "6376714797", - "name": "Desktop users" - }, - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"safari\"}]]]", - "id": "6381732153", - "name": "Safari users" - }, - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"opera\"}]]]", - "id": "6383110825", - "name": "Opera users" - }, - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"tablet\"}]]]", - "id": "6387291324", - "name": "Tablet users" - }, - { - "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " - "\"type\": \"custom_dimension\", \"value\": \"chrome\"}]]]", - "id": "6388221254", - "name": "Chrome users" - } - ], - "dimensions": [ - { - "id": "6380961481", - "key": "browser_type", - "segmentId": "6384711633" - } - ], - "groups": [ - { - "policy": "random", - "trafficAllocation": [ - { - "entityId": "6454500206", - "endOfRange": 5000 - }, - { - "entityId": "6456310069", - "endOfRange": 10000 - } - ], - "experiments": [ - { - "status": "Running", - "percentageIncluded": 5000, - "key": "mutex_exp1", - "trafficAllocation": [ - { - "entityId": "6413061880", - "endOfRange": 5000 - }, - { - "entityId": "6413061881", - "endOfRange": 10000 - } - ], - "audienceIds": [ - "6388221254" - ], - "variations": [ - { - "id": "6413061880", - "key": "a" - }, - { - "id": "6413061881", - "key": "b" - } - ], - "forcedVariations": { - - }, - "id": "6454500206" - }, - { - "status": "Running", - "percentageIncluded": 5000, - "key": "mutex_exp2", - "trafficAllocation": [ - { - "entityId": "6445960276", - "endOfRange": 5000 - }, - { - "entityId": "6445960277", - "endOfRange": 10000 - } - ], - "audienceIds": [ - - ], - "variations": [ - { - "id": "6445960276", - "key": "a" - }, - { - "id": "6445960277", - "key": "b" - } - ], - "forcedVariations": { - "user_b": "b", - "user_a": "a" - }, - "id": "6456310069" - } - ], - "id": "6455220163" - } - ], - "projectId": "6372300739", - "accountId": "6365361536", - "events": [ - { - "experimentIds": [ - "6359356006" - ], - "id": "6357247504", - "key": "testEventWithAudiences" - }, - { - "experimentIds": [ - "6456310069" - ], - "id": "6357622693", - "key": "testEventWithMultipleGroupedExperiments" - }, - { - "experimentIds": [ - "6375231238" - ], - "id": "6367473109", - "key": "testEventWithExperimentNotRunning" - }, - { - "experimentIds": [ - "6380251600" - ], - "id": "6370537431", - "key": "testEvent" - }, - { - "experimentIds": [ + "experiments": [ + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment4", + "trafficAllocation": [ + {"entityId": "6373141147", "endOfRange": 5000}, + {"entityId": "6373141148", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6373141147", "key": "control"}, {"id": "6373141148", "key": "variation"}], + "forcedVariations": {}, + "id": "6358043286", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment5", + "trafficAllocation": [ + {"entityId": "6335242053", "endOfRange": 5000}, + {"entityId": "6335242054", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6335242053", "key": "control"}, {"id": "6335242054", "key": "variation"}], + "forcedVariations": {}, + "id": "6364835526", + }, + { + "status": "Paused", + "percentageIncluded": 10000, + "key": "testExperimentNotRunning", + "trafficAllocation": [ + {"entityId": "6377281127", "endOfRange": 5000}, + {"entityId": "6377281128", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6377281127", "key": "control"}, {"id": "6377281128", "key": "variation"}], + "forcedVariations": {}, + "id": "6367444440", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment1", + "trafficAllocation": [ + {"entityId": "6384330451", "endOfRange": 5000}, + {"entityId": "6384330452", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6384330451", "key": "control"}, {"id": "6384330452", "key": "variation"}], + "forcedVariations": {"variation_user": "variation", "control_user": "control"}, + "id": "6367863211", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment3", + "trafficAllocation": [ + {"entityId": "6376141758", "endOfRange": 5000}, + {"entityId": "6376141759", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6376141758", "key": "control"}, {"id": "6376141759", "key": "variation"}], + "forcedVariations": {}, + "id": "6370392407", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment6", + "trafficAllocation": [ + {"entityId": "6379060914", "endOfRange": 5000}, + {"entityId": "6379060915", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6379060914", "key": "control"}, {"id": "6379060915", "key": "variation"}], + "forcedVariations": {"forced_variation_user": "variation"}, + "id": "6370821515", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment2", + "trafficAllocation": [ + {"entityId": "6386700062", "endOfRange": 5000}, + {"entityId": "6386700063", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6386700062", "key": "control"}, {"id": "6386700063", "key": "variation"}], + "forcedVariations": {"variation_user": "variation", "control_user": "control"}, + "id": "6376870125", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperimentWithFirefoxAudience", + "trafficAllocation": [ + {"entityId": "6333082303", "endOfRange": 5000}, + {"entityId": "6333082304", "endOfRange": 10000}, + ], + "audienceIds": ["6369992312"], + "variations": [{"id": "6333082303", "key": "control"}, {"id": "6333082304", "key": "variation"}], + "forcedVariations": {}, + "id": "6383811281", + }, + ], + "version": "1", + "audiences": [ + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"safari\"}]]]", + "id": "6352892614", + "name": "Safari users", + }, + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"android\"}]]]", + "id": "6355234780", + "name": "Android users", + }, + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"desktop\"}]]]", + "id": "6360574256", + "name": "Desktop users", + }, + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"opera\"}]]]", + "id": "6365864533", + "name": "Opera users", + }, + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"tablet\"}]]]", + "id": "6369831151", + "name": "Tablet users", + }, + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"firefox\"}]]]", + "id": "6369992312", + "name": "Firefox users", + }, + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"chrome\"}]]]", + "id": "6373141157", + "name": "Chrome users", + }, + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"ie\"}]]]", + "id": "6378191386", + "name": "IE users", + }, + ], + "dimensions": [{"id": "6359881003", "key": "browser_type", "segmentId": "6380740826"}], + "groups": [ + {"policy": "random", "trafficAllocation": [], "experiments": [], "id": "6367902163"}, + {"policy": "random", "trafficAllocation": [], "experiments": [], "id": "6393150032"}, + { + "policy": "random", + "trafficAllocation": [ + {"entityId": "6450630664", "endOfRange": 5000}, + {"entityId": "6447021179", "endOfRange": 10000}, + ], + "experiments": [ + { + "status": "Running", + "percentageIncluded": 5000, + "key": "mutex_exp2", + "trafficAllocation": [ + {"entityId": "6453410972", "endOfRange": 5000}, + {"entityId": "6453410973", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6453410972", "key": "a"}, {"id": "6453410973", "key": "b"}], + "forcedVariations": {"user_b": "b", "user_a": "a"}, + "id": "6447021179", + }, + { + "status": "Running", + "percentageIncluded": 5000, + "key": "mutex_exp1", + "trafficAllocation": [ + {"entityId": "6451680205", "endOfRange": 5000}, + {"entityId": "6451680206", "endOfRange": 10000}, + ], + "audienceIds": ["6373141157"], + "variations": [{"id": "6451680205", "key": "a"}, {"id": "6451680206", "key": "b"}], + "forcedVariations": {}, + "id": "6450630664", + }, + ], + "id": "6436903041", + }, + ], + "projectId": "6377970066", + "accountId": "6365361536", + "events": [ + { + "experimentIds": ["6450630664", "6447021179"], + "id": "6370392432", + "key": "testEventWithMultipleGroupedExperiments", + }, + {"experimentIds": ["6367863211"], "id": "6372590948", "key": "testEvent"}, + { + "experimentIds": [ + "6364835526", + "6450630664", + "6367863211", + "6376870125", + "6383811281", + "6358043286", + "6370392407", + "6367444440", + "6370821515", + "6447021179", + ], + "id": "6372952486", + "key": "testEventWithMultipleExperiments", + }, + {"experimentIds": ["6367444440"], "id": "6380961307", "key": "testEventWithExperimentNotRunning"}, + {"experimentIds": ["6383811281"], "id": "6384781388", "key": "testEventWithAudiences"}, + {"experimentIds": [], "id": "6386521015", "key": "testEventWithoutExperiments"}, + {"experimentIds": ["6450630664", "6383811281", "6376870125"], "id": "6316734272", "key": "Total Revenue"}, + ], + "revision": "83", +} - ], - "id": "6377001020", - "key": "testEventWithoutExperiments" - }, - { - "experimentIds": [ - "6375231238", - "6364882055", - "6382300680", - "6374064265", - "6363607946", - "6370815084", - "6360796560", - "6384861073", - "6380932373", - "6385551136", - "6376141968", - "6375595048", - "6384711622", - "6381732143", - "6332666164", - "6379205045", - "6382682166", - "6313973431", - "6381781177", - "6377001018", - "6387252155", - "6375494974", - "6338678719", - "6388170688", - "6456310069", - "6362476358", - "6362476359", - "6379205044", - "6382950966", - "6371742027", - "6367922509", - "6380251600", - "6355784786", - "6377723605", - "6366023126", - "6380971484", - "6381631585", - "6379841378", - "6377202148", - "6361743077", - "6359356006", - "6379900650", - "6361359596", - "6454500206", - "6383120500", - "6367902584", - "6338678718", - "6383430268", - "6376658685", - "6369992702" - ], - "id": "6385432091", - "key": "testEventWithMultipleExperiments" - }, - { - "experimentIds": [ - "6377001018", - "6359356006", - "6454500206" - ], - "id": "6370815083", - "key": "Total Revenue" - } - ], - "revision": "58" +config_25_exp = { + "experiments": [ + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment12", + "trafficAllocation": [ + {"entityId": "6387320950", "endOfRange": 5000}, + {"entityId": "6387320951", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6387320950", "key": "control"}, {"id": "6387320951", "key": "variation"}], + "forcedVariations": {}, + "id": "6344617435", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment19", + "trafficAllocation": [ + {"entityId": "6380932289", "endOfRange": 5000}, + {"entityId": "6380932290", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6380932289", "key": "control"}, {"id": "6380932290", "key": "variation"}], + "forcedVariations": {}, + "id": "6349682899", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment21", + "trafficAllocation": [ + {"entityId": "6356833706", "endOfRange": 5000}, + {"entityId": "6356833707", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6356833706", "key": "control"}, {"id": "6356833707", "key": "variation"}], + "forcedVariations": {}, + "id": "6350472041", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment7", + "trafficAllocation": [ + {"entityId": "6367863508", "endOfRange": 5000}, + {"entityId": "6367863509", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6367863508", "key": "control"}, {"id": "6367863509", "key": "variation"}], + "forcedVariations": {}, + "id": "6352512126", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment15", + "trafficAllocation": [ + {"entityId": "6379652128", "endOfRange": 5000}, + {"entityId": "6379652129", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6379652128", "key": "control"}, {"id": "6379652129", "key": "variation"}], + "forcedVariations": {}, + "id": "6357622647", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment16", + "trafficAllocation": [ + {"entityId": "6359551503", "endOfRange": 5000}, + {"entityId": "6359551504", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6359551503", "key": "control"}, {"id": "6359551504", "key": "variation"}], + "forcedVariations": {}, + "id": "6361100609", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment8", + "trafficAllocation": [ + {"entityId": "6378191496", "endOfRange": 5000}, + {"entityId": "6378191497", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6378191496", "key": "control"}, {"id": "6378191497", "key": "variation"}], + "forcedVariations": {}, + "id": "6361743021", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperimentWithFirefoxAudience", + "trafficAllocation": [ + {"entityId": "6380932291", "endOfRange": 5000}, + {"entityId": "6380932292", "endOfRange": 10000}, + ], + "audienceIds": ["6317864099"], + "variations": [{"id": "6380932291", "key": "control"}, {"id": "6380932292", "key": "variation"}], + "forcedVariations": {}, + "id": "6361931183", + }, + { + "status": "Not started", + "percentageIncluded": 10000, + "key": "testExperimentNotRunning", + "trafficAllocation": [ + {"entityId": "6377723538", "endOfRange": 5000}, + {"entityId": "6377723539", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6377723538", "key": "control"}, {"id": "6377723539", "key": "variation"}], + "forcedVariations": {}, + "id": "6362042330", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment5", + "trafficAllocation": [ + {"entityId": "6361100607", "endOfRange": 5000}, + {"entityId": "6361100608", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6361100607", "key": "control"}, {"id": "6361100608", "key": "variation"}], + "forcedVariations": {}, + "id": "6365780767", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment0", + "trafficAllocation": [ + {"entityId": "6379122883", "endOfRange": 5000}, + {"entityId": "6379122884", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6379122883", "key": "control"}, {"id": "6379122884", "key": "variation"}], + "forcedVariations": {}, + "id": "6366023085", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment2", + "trafficAllocation": [ + {"entityId": "6373980983", "endOfRange": 5000}, + {"entityId": "6373980984", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6373980983", "key": "control"}, {"id": "6373980984", "key": "variation"}], + "forcedVariations": {"variation_user": "variation", "control_user": "control"}, + "id": "6367473060", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment13", + "trafficAllocation": [ + {"entityId": "6361931181", "endOfRange": 5000}, + {"entityId": "6361931182", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6361931181", "key": "control"}, {"id": "6361931182", "key": "variation"}], + "forcedVariations": {}, + "id": "6367842673", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment18", + "trafficAllocation": [ + {"entityId": "6375121958", "endOfRange": 5000}, + {"entityId": "6375121959", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6375121958", "key": "control"}, {"id": "6375121959", "key": "variation"}], + "forcedVariations": {}, + "id": "6367902537", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment17", + "trafficAllocation": [ + {"entityId": "6353582033", "endOfRange": 5000}, + {"entityId": "6353582034", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6353582033", "key": "control"}, {"id": "6353582034", "key": "variation"}], + "forcedVariations": {}, + "id": "6368671885", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment11", + "trafficAllocation": [ + {"entityId": "6355235088", "endOfRange": 5000}, + {"entityId": "6355235089", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6355235088", "key": "control"}, {"id": "6355235089", "key": "variation"}], + "forcedVariations": {}, + "id": "6369512098", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment3", + "trafficAllocation": [ + {"entityId": "6355235086", "endOfRange": 5000}, + {"entityId": "6355235087", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6355235086", "key": "control"}, {"id": "6355235087", "key": "variation"}], + "forcedVariations": {}, + "id": "6371041921", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment10", + "trafficAllocation": [ + {"entityId": "6382231014", "endOfRange": 5000}, + {"entityId": "6382231015", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6382231014", "key": "control"}, {"id": "6382231015", "key": "variation"}], + "forcedVariations": {}, + "id": "6375231186", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment20", + "trafficAllocation": [ + {"entityId": "6362951972", "endOfRange": 5000}, + {"entityId": "6362951973", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6362951972", "key": "control"}, {"id": "6362951973", "key": "variation"}], + "forcedVariations": {}, + "id": "6377131549", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment9", + "trafficAllocation": [ + {"entityId": "6369462637", "endOfRange": 5000}, + {"entityId": "6369462638", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6369462637", "key": "control"}, {"id": "6369462638", "key": "variation"}], + "forcedVariations": {}, + "id": "6382251626", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment14", + "trafficAllocation": [ + {"entityId": "6388520034", "endOfRange": 5000}, + {"entityId": "6388520035", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6388520034", "key": "control"}, {"id": "6388520035", "key": "variation"}], + "forcedVariations": {}, + "id": "6383770101", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment6", + "trafficAllocation": [ + {"entityId": "6378802069", "endOfRange": 5000}, + {"entityId": "6378802070", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6378802069", "key": "control"}, {"id": "6378802070", "key": "variation"}], + "forcedVariations": {}, + "id": "6386411740", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment4", + "trafficAllocation": [ + {"entityId": "6350263010", "endOfRange": 5000}, + {"entityId": "6350263011", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6350263010", "key": "control"}, {"id": "6350263011", "key": "variation"}], + "forcedVariations": {}, + "id": "6386460951", + }, + ], + "version": "1", + "audiences": [ + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"firefox\"}]]]", + "id": "6317864099", + "name": "Firefox users", + }, + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"safari\"}]]]", + "id": "6360592016", + "name": "Safari users", + }, + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"chrome\"}]]]", + "id": "6361743063", + "name": "Chrome users", + }, + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"desktop\"}]]]", + "id": "6372190788", + "name": "Desktop users", + }, + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"android\"}]]]", + "id": "6376141951", + "name": "Android users", + }, + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"ie\"}]]]", + "id": "6377605300", + "name": "IE users", + }, + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"tablet\"}]]]", + "id": "6378191534", + "name": "Tablet users", + }, + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"opera\"}]]]", + "id": "6386521201", + "name": "Opera users", + }, + ], + "dimensions": [{"id": "6381732124", "key": "browser_type", "segmentId": "6388221232"}], + "groups": [ + { + "policy": "random", + "trafficAllocation": [ + {"entityId": "6416416234", "endOfRange": 5000}, + {"entityId": "6451651052", "endOfRange": 10000}, + ], + "experiments": [ + { + "status": "Running", + "percentageIncluded": 5000, + "key": "mutex_exp1", + "trafficAllocation": [ + {"entityId": "6448110056", "endOfRange": 5000}, + {"entityId": "6448110057", "endOfRange": 10000}, + ], + "audienceIds": ["6361743063"], + "variations": [{"id": "6448110056", "key": "a"}, {"id": "6448110057", "key": "b"}], + "forcedVariations": {}, + "id": "6416416234", + }, + { + "status": "Running", + "percentageIncluded": 5000, + "key": "mutex_exp2", + "trafficAllocation": [ + {"entityId": "6437485007", "endOfRange": 5000}, + {"entityId": "6437485008", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6437485007", "key": "a"}, {"id": "6437485008", "key": "b"}], + "forcedVariations": {"user_b": "b", "user_a": "a"}, + "id": "6451651052", + }, + ], + "id": "6441101079", + } + ], + "projectId": "6379191198", + "accountId": "6365361536", + "events": [ + {"experimentIds": [], "id": "6360377431", "key": "testEventWithoutExperiments"}, + {"experimentIds": ["6366023085"], "id": "6373184839", "key": "testEvent"}, + {"experimentIds": ["6451651052"], "id": "6379061102", "key": "testEventWithMultipleGroupedExperiments"}, + {"experimentIds": ["6362042330"], "id": "6385201698", "key": "testEventWithExperimentNotRunning"}, + {"experimentIds": ["6361931183"], "id": "6385551103", "key": "testEventWithAudiences"}, + { + "experimentIds": [ + "6371041921", + "6382251626", + "6368671885", + "6361743021", + "6386460951", + "6377131549", + "6365780767", + "6369512098", + "6367473060", + "6366023085", + "6361931183", + "6361100609", + "6367902537", + "6375231186", + "6349682899", + "6362042330", + "6344617435", + "6386411740", + "6350472041", + "6416416234", + "6451651052", + "6367842673", + "6383770101", + "6357622647", + "6352512126", + ], + "id": "6386470923", + "key": "testEventWithMultipleExperiments", + }, + {"experimentIds": ["6361931183", "6416416234", "6367473060"], "id": "6386460946", "key": "Total Revenue"}, + ], + "revision": "92", } -datafiles = { - 10: config_10_exp, - 25: config_25_exp, - 50: config_50_exp +config_50_exp = { + "experiments": [ + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment31", + "trafficAllocation": [ + {"entityId": "6383523065", "endOfRange": 5000}, + {"entityId": "6383523066", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6383523065", "key": "control"}, {"id": "6383523066", "key": "variation"}], + "forcedVariations": {}, + "id": "6313973431", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment15", + "trafficAllocation": [ + {"entityId": "6363413697", "endOfRange": 5000}, + {"entityId": "6363413698", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6363413697", "key": "control"}, {"id": "6363413698", "key": "variation"}], + "forcedVariations": {}, + "id": "6332666164", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment33", + "trafficAllocation": [ + {"entityId": "6330789404", "endOfRange": 5000}, + {"entityId": "6330789405", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6330789404", "key": "control"}, {"id": "6330789405", "key": "variation"}], + "forcedVariations": {}, + "id": "6338678718", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment38", + "trafficAllocation": [ + {"entityId": "6376706101", "endOfRange": 5000}, + {"entityId": "6376706102", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6376706101", "key": "control"}, {"id": "6376706102", "key": "variation"}], + "forcedVariations": {}, + "id": "6338678719", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment44", + "trafficAllocation": [ + {"entityId": "6316734590", "endOfRange": 5000}, + {"entityId": "6316734591", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6316734590", "key": "control"}, {"id": "6316734591", "key": "variation"}], + "forcedVariations": {}, + "id": "6355784786", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperimentWithFirefoxAudience", + "trafficAllocation": [ + {"entityId": "6362476365", "endOfRange": 5000}, + {"entityId": "6362476366", "endOfRange": 10000}, + ], + "audienceIds": ["6373742627"], + "variations": [{"id": "6362476365", "key": "control"}, {"id": "6362476366", "key": "variation"}], + "forcedVariations": {}, + "id": "6359356006", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment14", + "trafficAllocation": [ + {"entityId": "6327476066", "endOfRange": 5000}, + {"entityId": "6327476067", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6327476066", "key": "control"}, {"id": "6327476067", "key": "variation"}], + "forcedVariations": {}, + "id": "6360796560", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment46", + "trafficAllocation": [ + {"entityId": "6357247500", "endOfRange": 5000}, + {"entityId": "6357247501", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6357247500", "key": "control"}, {"id": "6357247501", "key": "variation"}], + "forcedVariations": {}, + "id": "6361359596", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment16", + "trafficAllocation": [ + {"entityId": "6378191544", "endOfRange": 5000}, + {"entityId": "6378191545", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6378191544", "key": "control"}, {"id": "6378191545", "key": "variation"}], + "forcedVariations": {}, + "id": "6361743077", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment10", + "trafficAllocation": [ + {"entityId": "6372300744", "endOfRange": 5000}, + {"entityId": "6372300745", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6372300744", "key": "control"}, {"id": "6372300745", "key": "variation"}], + "forcedVariations": {}, + "id": "6362476358", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment11", + "trafficAllocation": [ + {"entityId": "6357247497", "endOfRange": 5000}, + {"entityId": "6357247498", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6357247497", "key": "control"}, {"id": "6357247498", "key": "variation"}], + "forcedVariations": {}, + "id": "6362476359", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment12", + "trafficAllocation": [ + {"entityId": "6368497829", "endOfRange": 5000}, + {"entityId": "6368497830", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6368497829", "key": "control"}, {"id": "6368497830", "key": "variation"}], + "forcedVariations": {}, + "id": "6363607946", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment7", + "trafficAllocation": [ + {"entityId": "6386590519", "endOfRange": 5000}, + {"entityId": "6386590520", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6386590519", "key": "control"}, {"id": "6386590520", "key": "variation"}], + "forcedVariations": {}, + "id": "6364882055", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment6", + "trafficAllocation": [ + {"entityId": "6385481560", "endOfRange": 5000}, + {"entityId": "6385481561", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6385481560", "key": "control"}, {"id": "6385481561", "key": "variation"}], + "forcedVariations": {}, + "id": "6366023126", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment23", + "trafficAllocation": [ + {"entityId": "6375122007", "endOfRange": 5000}, + {"entityId": "6375122008", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6375122007", "key": "control"}, {"id": "6375122008", "key": "variation"}], + "forcedVariations": {}, + "id": "6367902584", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment13", + "trafficAllocation": [ + {"entityId": "6360762679", "endOfRange": 5000}, + {"entityId": "6360762680", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6360762679", "key": "control"}, {"id": "6360762680", "key": "variation"}], + "forcedVariations": {}, + "id": "6367922509", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment39", + "trafficAllocation": [ + {"entityId": "6341311988", "endOfRange": 5000}, + {"entityId": "6341311989", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6341311988", "key": "control"}, {"id": "6341311989", "key": "variation"}], + "forcedVariations": {}, + "id": "6369992702", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment4", + "trafficAllocation": [ + {"entityId": "6370014876", "endOfRange": 5000}, + {"entityId": "6370014877", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6370014876", "key": "control"}, {"id": "6370014877", "key": "variation"}], + "forcedVariations": {}, + "id": "6370815084", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment17", + "trafficAllocation": [ + {"entityId": "6384651930", "endOfRange": 5000}, + {"entityId": "6384651931", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6384651930", "key": "control"}, {"id": "6384651931", "key": "variation"}], + "forcedVariations": {}, + "id": "6371742027", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment42", + "trafficAllocation": [ + {"entityId": "6371581616", "endOfRange": 5000}, + {"entityId": "6371581617", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6371581616", "key": "control"}, {"id": "6371581617", "key": "variation"}], + "forcedVariations": {}, + "id": "6374064265", + }, + { + "status": "Not started", + "percentageIncluded": 10000, + "key": "testExperimentNotRunning", + "trafficAllocation": [ + {"entityId": "6380740985", "endOfRange": 5000}, + {"entityId": "6380740986", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6380740985", "key": "control"}, {"id": "6380740986", "key": "variation"}], + "forcedVariations": {}, + "id": "6375231238", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment36", + "trafficAllocation": [ + {"entityId": "6380164945", "endOfRange": 5000}, + {"entityId": "6380164946", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6380164945", "key": "control"}, {"id": "6380164946", "key": "variation"}], + "forcedVariations": {}, + "id": "6375494974", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment45", + "trafficAllocation": [ + {"entityId": "6374765096", "endOfRange": 5000}, + {"entityId": "6374765097", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6374765096", "key": "control"}, {"id": "6374765097", "key": "variation"}], + "forcedVariations": {}, + "id": "6375595048", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment43", + "trafficAllocation": [ + {"entityId": "6385191624", "endOfRange": 5000}, + {"entityId": "6385191625", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6385191624", "key": "control"}, {"id": "6385191625", "key": "variation"}], + "forcedVariations": {}, + "id": "6376141968", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment25", + "trafficAllocation": [ + {"entityId": "6368955066", "endOfRange": 5000}, + {"entityId": "6368955067", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6368955066", "key": "control"}, {"id": "6368955067", "key": "variation"}], + "forcedVariations": {}, + "id": "6376658685", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment2", + "trafficAllocation": [ + {"entityId": "6382040994", "endOfRange": 5000}, + {"entityId": "6382040995", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6382040994", "key": "control"}, {"id": "6382040995", "key": "variation"}], + "forcedVariations": {"variation_user": "variation", "control_user": "control"}, + "id": "6377001018", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment18", + "trafficAllocation": [ + {"entityId": "6370582521", "endOfRange": 5000}, + {"entityId": "6370582522", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6370582521", "key": "control"}, {"id": "6370582522", "key": "variation"}], + "forcedVariations": {}, + "id": "6377202148", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment24", + "trafficAllocation": [ + {"entityId": "6381612278", "endOfRange": 5000}, + {"entityId": "6381612279", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6381612278", "key": "control"}, {"id": "6381612279", "key": "variation"}], + "forcedVariations": {}, + "id": "6377723605", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment19", + "trafficAllocation": [ + {"entityId": "6362476361", "endOfRange": 5000}, + {"entityId": "6362476362", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6362476361", "key": "control"}, {"id": "6362476362", "key": "variation"}], + "forcedVariations": {}, + "id": "6379205044", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment20", + "trafficAllocation": [ + {"entityId": "6370537428", "endOfRange": 5000}, + {"entityId": "6370537429", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6370537428", "key": "control"}, {"id": "6370537429", "key": "variation"}], + "forcedVariations": {}, + "id": "6379205045", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment28", + "trafficAllocation": [ + {"entityId": "6387291313", "endOfRange": 5000}, + {"entityId": "6387291314", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6387291313", "key": "control"}, {"id": "6387291314", "key": "variation"}], + "forcedVariations": {}, + "id": "6379841378", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment35", + "trafficAllocation": [ + {"entityId": "6375332081", "endOfRange": 5000}, + {"entityId": "6375332082", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6375332081", "key": "control"}, {"id": "6375332082", "key": "variation"}], + "forcedVariations": {}, + "id": "6379900650", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment1", + "trafficAllocation": [ + {"entityId": "6355235181", "endOfRange": 5000}, + {"entityId": "6355235182", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6355235181", "key": "control"}, {"id": "6355235182", "key": "variation"}], + "forcedVariations": {"variation_user": "variation", "control_user": "control"}, + "id": "6380251600", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment8", + "trafficAllocation": [ + {"entityId": "6310506102", "endOfRange": 5000}, + {"entityId": "6310506103", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6310506102", "key": "control"}, {"id": "6310506103", "key": "variation"}], + "forcedVariations": {}, + "id": "6380932373", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment3", + "trafficAllocation": [ + {"entityId": "6373612240", "endOfRange": 5000}, + {"entityId": "6373612241", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6373612240", "key": "control"}, {"id": "6373612241", "key": "variation"}], + "forcedVariations": {}, + "id": "6380971484", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment22", + "trafficAllocation": [ + {"entityId": "6360796561", "endOfRange": 5000}, + {"entityId": "6360796562", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6360796561", "key": "control"}, {"id": "6360796562", "key": "variation"}], + "forcedVariations": {}, + "id": "6381631585", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment37", + "trafficAllocation": [ + {"entityId": "6356824684", "endOfRange": 5000}, + {"entityId": "6356824685", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6356824684", "key": "control"}, {"id": "6356824685", "key": "variation"}], + "forcedVariations": {}, + "id": "6381732143", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment41", + "trafficAllocation": [ + {"entityId": "6389170550", "endOfRange": 5000}, + {"entityId": "6389170551", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6389170550", "key": "control"}, {"id": "6389170551", "key": "variation"}], + "forcedVariations": {}, + "id": "6381781177", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment27", + "trafficAllocation": [ + {"entityId": "6372591085", "endOfRange": 5000}, + {"entityId": "6372591086", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6372591085", "key": "control"}, {"id": "6372591086", "key": "variation"}], + "forcedVariations": {}, + "id": "6382300680", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment26", + "trafficAllocation": [ + {"entityId": "6375602097", "endOfRange": 5000}, + {"entityId": "6375602098", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6375602097", "key": "control"}, {"id": "6375602098", "key": "variation"}], + "forcedVariations": {}, + "id": "6382682166", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment9", + "trafficAllocation": [ + {"entityId": "6376221556", "endOfRange": 5000}, + {"entityId": "6376221557", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6376221556", "key": "control"}, {"id": "6376221557", "key": "variation"}], + "forcedVariations": {}, + "id": "6382950966", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment29", + "trafficAllocation": [ + {"entityId": "6382070548", "endOfRange": 5000}, + {"entityId": "6382070549", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6382070548", "key": "control"}, {"id": "6382070549", "key": "variation"}], + "forcedVariations": {}, + "id": "6383120500", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment32", + "trafficAllocation": [ + {"entityId": "6391210101", "endOfRange": 5000}, + {"entityId": "6391210102", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6391210101", "key": "control"}, {"id": "6391210102", "key": "variation"}], + "forcedVariations": {}, + "id": "6383430268", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment30", + "trafficAllocation": [ + {"entityId": "6364835927", "endOfRange": 5000}, + {"entityId": "6364835928", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6364835927", "key": "control"}, {"id": "6364835928", "key": "variation"}], + "forcedVariations": {}, + "id": "6384711622", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment34", + "trafficAllocation": [ + {"entityId": "6390151025", "endOfRange": 5000}, + {"entityId": "6390151026", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6390151025", "key": "control"}, {"id": "6390151026", "key": "variation"}], + "forcedVariations": {}, + "id": "6384861073", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment21", + "trafficAllocation": [ + {"entityId": "6384881124", "endOfRange": 5000}, + {"entityId": "6384881125", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6384881124", "key": "control"}, {"id": "6384881125", "key": "variation"}], + "forcedVariations": {}, + "id": "6385551136", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment40", + "trafficAllocation": [ + {"entityId": "6387261935", "endOfRange": 5000}, + {"entityId": "6387261936", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6387261935", "key": "control"}, {"id": "6387261936", "key": "variation"}], + "forcedVariations": {}, + "id": "6387252155", + }, + { + "status": "Running", + "percentageIncluded": 10000, + "key": "testExperiment5", + "trafficAllocation": [ + {"entityId": "6312093242", "endOfRange": 5000}, + {"entityId": "6312093243", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6312093242", "key": "control"}, {"id": "6312093243", "key": "variation"}], + "forcedVariations": {}, + "id": "6388170688", + }, + ], + "version": "1", + "audiences": [ + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"android\"}]]]", + "id": "6366023138", + "name": "Android users", + }, + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"firefox\"}]]]", + "id": "6373742627", + "name": "Firefox users", + }, + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"ie\"}]]]", + "id": "6376161539", + "name": "IE users", + }, + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"desktop\"}]]]", + "id": "6376714797", + "name": "Desktop users", + }, + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"safari\"}]]]", + "id": "6381732153", + "name": "Safari users", + }, + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"opera\"}]]]", + "id": "6383110825", + "name": "Opera users", + }, + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"tablet\"}]]]", + "id": "6387291324", + "name": "Tablet users", + }, + { + "conditions": "[\"and\", [\"or\", [\"or\", {\"name\": \"browser_type\", " + "\"type\": \"custom_dimension\", \"value\": \"chrome\"}]]]", + "id": "6388221254", + "name": "Chrome users", + }, + ], + "dimensions": [{"id": "6380961481", "key": "browser_type", "segmentId": "6384711633"}], + "groups": [ + { + "policy": "random", + "trafficAllocation": [ + {"entityId": "6454500206", "endOfRange": 5000}, + {"entityId": "6456310069", "endOfRange": 10000}, + ], + "experiments": [ + { + "status": "Running", + "percentageIncluded": 5000, + "key": "mutex_exp1", + "trafficAllocation": [ + {"entityId": "6413061880", "endOfRange": 5000}, + {"entityId": "6413061881", "endOfRange": 10000}, + ], + "audienceIds": ["6388221254"], + "variations": [{"id": "6413061880", "key": "a"}, {"id": "6413061881", "key": "b"}], + "forcedVariations": {}, + "id": "6454500206", + }, + { + "status": "Running", + "percentageIncluded": 5000, + "key": "mutex_exp2", + "trafficAllocation": [ + {"entityId": "6445960276", "endOfRange": 5000}, + {"entityId": "6445960277", "endOfRange": 10000}, + ], + "audienceIds": [], + "variations": [{"id": "6445960276", "key": "a"}, {"id": "6445960277", "key": "b"}], + "forcedVariations": {"user_b": "b", "user_a": "a"}, + "id": "6456310069", + }, + ], + "id": "6455220163", + } + ], + "projectId": "6372300739", + "accountId": "6365361536", + "events": [ + {"experimentIds": ["6359356006"], "id": "6357247504", "key": "testEventWithAudiences"}, + {"experimentIds": ["6456310069"], "id": "6357622693", "key": "testEventWithMultipleGroupedExperiments"}, + {"experimentIds": ["6375231238"], "id": "6367473109", "key": "testEventWithExperimentNotRunning"}, + {"experimentIds": ["6380251600"], "id": "6370537431", "key": "testEvent"}, + {"experimentIds": [], "id": "6377001020", "key": "testEventWithoutExperiments"}, + { + "experimentIds": [ + "6375231238", + "6364882055", + "6382300680", + "6374064265", + "6363607946", + "6370815084", + "6360796560", + "6384861073", + "6380932373", + "6385551136", + "6376141968", + "6375595048", + "6384711622", + "6381732143", + "6332666164", + "6379205045", + "6382682166", + "6313973431", + "6381781177", + "6377001018", + "6387252155", + "6375494974", + "6338678719", + "6388170688", + "6456310069", + "6362476358", + "6362476359", + "6379205044", + "6382950966", + "6371742027", + "6367922509", + "6380251600", + "6355784786", + "6377723605", + "6366023126", + "6380971484", + "6381631585", + "6379841378", + "6377202148", + "6361743077", + "6359356006", + "6379900650", + "6361359596", + "6454500206", + "6383120500", + "6367902584", + "6338678718", + "6383430268", + "6376658685", + "6369992702", + ], + "id": "6385432091", + "key": "testEventWithMultipleExperiments", + }, + {"experimentIds": ["6377001018", "6359356006", "6454500206"], "id": "6370815083", "key": "Total Revenue"}, + ], + "revision": "58", } +datafiles = {10: config_10_exp, 25: config_25_exp, 50: config_50_exp} + def create_optimizely_object(datafile): - """ Helper method to create and return Optimizely object. """ + """ Helper method to create and return Optimizely object. """ - class NoOpEventDispatcher(object): - @staticmethod - def dispatch_event(url, params): - """ No op event dispatcher. + class NoOpEventDispatcher(object): + @staticmethod + def dispatch_event(url, params): + """ No op event dispatcher. Args: url: URL to send impression/conversion event to. params: Params to be sent to the impression/conversion event. """ - pass - return optimizely.Optimizely(datafile, event_dispatcher=NoOpEventDispatcher) + pass + + return optimizely.Optimizely(datafile, event_dispatcher=NoOpEventDispatcher) optimizely_obj_10_exp = create_optimizely_object(json.dumps(datafiles.get(10))) @@ -3287,104 +1496,96 @@ def dispatch_event(url, params): optimizely_obj_50_exp = create_optimizely_object(json.dumps(datafiles.get(50))) test_data = { - 'create_object': { - 10: [datafiles.get(10)], - 25: [datafiles.get(25)], - 50: [datafiles.get(50)] - }, - 'create_object_schema_validation_off': { - 10: [datafiles.get(10)], - 25: [datafiles.get(25)], - 50: [datafiles.get(50)] - }, - 'activate_with_no_attributes': { - 10: [optimizely_obj_10_exp, 'test'], - 25: [optimizely_obj_25_exp, 'optimizely_user'], - 50: [optimizely_obj_50_exp, 'optimizely_user'] - }, - 'activate_with_attributes': { - 10: [optimizely_obj_10_exp, 'optimizely_user'], - 25: [optimizely_obj_25_exp, 'optimizely_user'], - 50: [optimizely_obj_50_exp, 'test'] - }, - 'activate_with_forced_variation': { - 10: [optimizely_obj_10_exp, 'variation_user'], - 25: [optimizely_obj_25_exp, 'variation_user'], - 50: [optimizely_obj_50_exp, 'variation_user'] - }, - 'activate_grouped_experiment_no_attributes': { - 10: [optimizely_obj_10_exp, 'no'], - 25: [optimizely_obj_25_exp, 'test'], - 50: [optimizely_obj_50_exp, 'optimizely_user'] - }, - 'activate_grouped_experiment_with_attributes': { - 10: [optimizely_obj_10_exp, 'test'], - 25: [optimizely_obj_25_exp, 'yes'], - 50: [optimizely_obj_50_exp, 'test'] - }, - 'get_variation_with_no_attributes': { - 10: [optimizely_obj_10_exp, 'test'], - 25: [optimizely_obj_25_exp, 'optimizely_user'], - 50: [optimizely_obj_50_exp, 'optimizely_user'] - }, - 'get_variation_with_attributes': { - 10: [optimizely_obj_10_exp, 'optimizely_user'], - 25: [optimizely_obj_25_exp, 'optimizely_user'], - 50: [optimizely_obj_50_exp, 'test'] - }, - 'get_variation_with_forced_variation': { - 10: [optimizely_obj_10_exp, 'variation_user'], - 25: [optimizely_obj_25_exp, 'variation_user'], - 50: [optimizely_obj_50_exp, 'variation_user'] - }, - 'get_variation_grouped_experiment_no_attributes': { - 10: [optimizely_obj_10_exp, 'no'], - 25: [optimizely_obj_25_exp, 'test'], - 50: [optimizely_obj_50_exp, 'optimizely_user'] - }, - 'get_variation_grouped_experiment_with_attributes': { - 10: [optimizely_obj_10_exp, 'test'], - 25: [optimizely_obj_25_exp, 'yes'], - 50: [optimizely_obj_50_exp, 'test'] - }, - 'track_with_attributes': { - 10: [optimizely_obj_10_exp, 'optimizely_user'], - 25: [optimizely_obj_25_exp, 'optimizely_user'], - 50: [optimizely_obj_50_exp, 'optimizely_user'] - }, - 'track_with_revenue': { - 10: [optimizely_obj_10_exp, 'optimizely_user'], - 25: [optimizely_obj_25_exp, 'optimizely_user'], - 50: [optimizely_obj_50_exp, 'optimizely_user'] - }, - 'track_with_attributes_and_revenue': { - 10: [optimizely_obj_10_exp, 'optimizely_user'], - 25: [optimizely_obj_25_exp, 'optimizely_user'], - 50: [optimizely_obj_50_exp, 'optimizely_user'] - }, - 'track_no_attributes_no_revenue': { - 10: [optimizely_obj_10_exp, 'optimizely_user'], - 25: [optimizely_obj_25_exp, 'optimizely_user'], - 50: [optimizely_obj_50_exp, 'optimizely_user'] - }, - 'track_grouped_experiment': { - 10: [optimizely_obj_10_exp, 'no'], - 25: [optimizely_obj_25_exp, 'optimizely_user'], - 50: [optimizely_obj_50_exp, 'optimizely_user'] - }, - 'track_grouped_experiment_with_attributes': { - 10: [optimizely_obj_10_exp, 'optimizely_user'], - 25: [optimizely_obj_25_exp, 'yes'], - 50: [optimizely_obj_50_exp, 'test'] - }, - 'track_grouped_experiment_with_revenue': { - 10: [optimizely_obj_10_exp, 'no'], - 25: [optimizely_obj_25_exp, 'optimizely_user'], - 50: [optimizely_obj_50_exp, 'optimizely_user'] - }, - 'track_grouped_experiment_with_attributes_and_revenue': { - 10: [optimizely_obj_10_exp, 'optimizely_user'], - 25: [optimizely_obj_25_exp, 'yes'], - 50: [optimizely_obj_50_exp, 'test'] - }, + 'create_object': {10: [datafiles.get(10)], 25: [datafiles.get(25)], 50: [datafiles.get(50)]}, + 'create_object_schema_validation_off': {10: [datafiles.get(10)], 25: [datafiles.get(25)], 50: [datafiles.get(50)]}, + 'activate_with_no_attributes': { + 10: [optimizely_obj_10_exp, 'test'], + 25: [optimizely_obj_25_exp, 'optimizely_user'], + 50: [optimizely_obj_50_exp, 'optimizely_user'], + }, + 'activate_with_attributes': { + 10: [optimizely_obj_10_exp, 'optimizely_user'], + 25: [optimizely_obj_25_exp, 'optimizely_user'], + 50: [optimizely_obj_50_exp, 'test'], + }, + 'activate_with_forced_variation': { + 10: [optimizely_obj_10_exp, 'variation_user'], + 25: [optimizely_obj_25_exp, 'variation_user'], + 50: [optimizely_obj_50_exp, 'variation_user'], + }, + 'activate_grouped_experiment_no_attributes': { + 10: [optimizely_obj_10_exp, 'no'], + 25: [optimizely_obj_25_exp, 'test'], + 50: [optimizely_obj_50_exp, 'optimizely_user'], + }, + 'activate_grouped_experiment_with_attributes': { + 10: [optimizely_obj_10_exp, 'test'], + 25: [optimizely_obj_25_exp, 'yes'], + 50: [optimizely_obj_50_exp, 'test'], + }, + 'get_variation_with_no_attributes': { + 10: [optimizely_obj_10_exp, 'test'], + 25: [optimizely_obj_25_exp, 'optimizely_user'], + 50: [optimizely_obj_50_exp, 'optimizely_user'], + }, + 'get_variation_with_attributes': { + 10: [optimizely_obj_10_exp, 'optimizely_user'], + 25: [optimizely_obj_25_exp, 'optimizely_user'], + 50: [optimizely_obj_50_exp, 'test'], + }, + 'get_variation_with_forced_variation': { + 10: [optimizely_obj_10_exp, 'variation_user'], + 25: [optimizely_obj_25_exp, 'variation_user'], + 50: [optimizely_obj_50_exp, 'variation_user'], + }, + 'get_variation_grouped_experiment_no_attributes': { + 10: [optimizely_obj_10_exp, 'no'], + 25: [optimizely_obj_25_exp, 'test'], + 50: [optimizely_obj_50_exp, 'optimizely_user'], + }, + 'get_variation_grouped_experiment_with_attributes': { + 10: [optimizely_obj_10_exp, 'test'], + 25: [optimizely_obj_25_exp, 'yes'], + 50: [optimizely_obj_50_exp, 'test'], + }, + 'track_with_attributes': { + 10: [optimizely_obj_10_exp, 'optimizely_user'], + 25: [optimizely_obj_25_exp, 'optimizely_user'], + 50: [optimizely_obj_50_exp, 'optimizely_user'], + }, + 'track_with_revenue': { + 10: [optimizely_obj_10_exp, 'optimizely_user'], + 25: [optimizely_obj_25_exp, 'optimizely_user'], + 50: [optimizely_obj_50_exp, 'optimizely_user'], + }, + 'track_with_attributes_and_revenue': { + 10: [optimizely_obj_10_exp, 'optimizely_user'], + 25: [optimizely_obj_25_exp, 'optimizely_user'], + 50: [optimizely_obj_50_exp, 'optimizely_user'], + }, + 'track_no_attributes_no_revenue': { + 10: [optimizely_obj_10_exp, 'optimizely_user'], + 25: [optimizely_obj_25_exp, 'optimizely_user'], + 50: [optimizely_obj_50_exp, 'optimizely_user'], + }, + 'track_grouped_experiment': { + 10: [optimizely_obj_10_exp, 'no'], + 25: [optimizely_obj_25_exp, 'optimizely_user'], + 50: [optimizely_obj_50_exp, 'optimizely_user'], + }, + 'track_grouped_experiment_with_attributes': { + 10: [optimizely_obj_10_exp, 'optimizely_user'], + 25: [optimizely_obj_25_exp, 'yes'], + 50: [optimizely_obj_50_exp, 'test'], + }, + 'track_grouped_experiment_with_revenue': { + 10: [optimizely_obj_10_exp, 'no'], + 25: [optimizely_obj_25_exp, 'optimizely_user'], + 50: [optimizely_obj_50_exp, 'optimizely_user'], + }, + 'track_grouped_experiment_with_attributes_and_revenue': { + 10: [optimizely_obj_10_exp, 'optimizely_user'], + 25: [optimizely_obj_25_exp, 'yes'], + 50: [optimizely_obj_50_exp, 'test'], + }, } diff --git a/tests/helpers_tests/test_audience.py b/tests/helpers_tests/test_audience.py index 4a586f4d..2beaf2cd 100644 --- a/tests/helpers_tests/test_audience.py +++ b/tests/helpers_tests/test_audience.py @@ -20,250 +20,305 @@ class AudienceTest(base.BaseTest): - - def setUp(self): - base.BaseTest.setUp(self) - self.mock_client_logger = mock.MagicMock() - - def test_is_user_in_experiment__no_audience(self): - """ Test that is_user_in_experiment returns True when experiment is using no audience. """ - - user_attributes = {} - - # Both Audience Ids and Conditions are Empty - experiment = self.project_config.get_experiment_from_key('test_experiment') - experiment.audienceIds = [] - experiment.audienceConditions = [] - self.assertStrictTrue(audience.is_user_in_experiment(self.project_config, - experiment, user_attributes, self.mock_client_logger)) - - # Audience Ids exist but Audience Conditions is Empty - experiment = self.project_config.get_experiment_from_key('test_experiment') - experiment.audienceIds = ['11154'] - experiment.audienceConditions = [] - self.assertStrictTrue(audience.is_user_in_experiment(self.project_config, - experiment, user_attributes, self.mock_client_logger)) - - # Audience Ids is Empty and Audience Conditions is None - experiment = self.project_config.get_experiment_from_key('test_experiment') - experiment.audienceIds = [] - experiment.audienceConditions = None - self.assertStrictTrue(audience.is_user_in_experiment(self.project_config, - experiment, user_attributes, self.mock_client_logger)) - - def test_is_user_in_experiment__with_audience(self): - """ Test that is_user_in_experiment evaluates non-empty audience. + def setUp(self): + base.BaseTest.setUp(self) + self.mock_client_logger = mock.MagicMock() + + def test_is_user_in_experiment__no_audience(self): + """ Test that is_user_in_experiment returns True when experiment is using no audience. """ + + user_attributes = {} + + # Both Audience Ids and Conditions are Empty + experiment = self.project_config.get_experiment_from_key('test_experiment') + experiment.audienceIds = [] + experiment.audienceConditions = [] + self.assertStrictTrue( + audience.is_user_in_experiment(self.project_config, experiment, user_attributes, self.mock_client_logger,) + ) + + # Audience Ids exist but Audience Conditions is Empty + experiment = self.project_config.get_experiment_from_key('test_experiment') + experiment.audienceIds = ['11154'] + experiment.audienceConditions = [] + self.assertStrictTrue( + audience.is_user_in_experiment(self.project_config, experiment, user_attributes, self.mock_client_logger,) + ) + + # Audience Ids is Empty and Audience Conditions is None + experiment = self.project_config.get_experiment_from_key('test_experiment') + experiment.audienceIds = [] + experiment.audienceConditions = None + self.assertStrictTrue( + audience.is_user_in_experiment(self.project_config, experiment, user_attributes, self.mock_client_logger,) + ) + + def test_is_user_in_experiment__with_audience(self): + """ Test that is_user_in_experiment evaluates non-empty audience. Test that is_user_in_experiment uses not None audienceConditions and ignores audienceIds. Test that is_user_in_experiment uses audienceIds when audienceConditions is None. """ - user_attributes = {'test_attribute': 'test_value_1'} - experiment = self.project_config.get_experiment_from_key('test_experiment') - experiment.audienceIds = ['11154'] + user_attributes = {'test_attribute': 'test_value_1'} + experiment = self.project_config.get_experiment_from_key('test_experiment') + experiment.audienceIds = ['11154'] - # Both Audience Ids and Conditions exist - with mock.patch('optimizely.helpers.condition_tree_evaluator.evaluate') as cond_tree_eval: + # Both Audience Ids and Conditions exist + with mock.patch('optimizely.helpers.condition_tree_evaluator.evaluate') as cond_tree_eval: - experiment.audienceConditions = ['and', ['or', '3468206642', '3988293898'], ['or', '3988293899', - '3468206646', '3468206647', '3468206644', '3468206643']] - audience.is_user_in_experiment(self.project_config, experiment, user_attributes, self.mock_client_logger) + experiment.audienceConditions = [ + 'and', + ['or', '3468206642', '3988293898'], + ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643'], + ] + audience.is_user_in_experiment( + self.project_config, experiment, user_attributes, self.mock_client_logger, + ) - self.assertEqual(experiment.audienceConditions, - cond_tree_eval.call_args[0][0]) + self.assertEqual(experiment.audienceConditions, cond_tree_eval.call_args[0][0]) - # Audience Ids exist but Audience Conditions is None - with mock.patch('optimizely.helpers.condition_tree_evaluator.evaluate') as cond_tree_eval: + # Audience Ids exist but Audience Conditions is None + with mock.patch('optimizely.helpers.condition_tree_evaluator.evaluate') as cond_tree_eval: - experiment.audienceConditions = None - audience.is_user_in_experiment(self.project_config, experiment, user_attributes, self.mock_client_logger) + experiment.audienceConditions = None + audience.is_user_in_experiment( + self.project_config, experiment, user_attributes, self.mock_client_logger, + ) - self.assertEqual(experiment.audienceIds, - cond_tree_eval.call_args[0][0]) + self.assertEqual(experiment.audienceIds, cond_tree_eval.call_args[0][0]) - def test_is_user_in_experiment__no_attributes(self): - """ Test that is_user_in_experiment evaluates audience when attributes are empty. + def test_is_user_in_experiment__no_attributes(self): + """ Test that is_user_in_experiment evaluates audience when attributes are empty. Test that is_user_in_experiment defaults attributes to empty dict when attributes is None. """ - experiment = self.project_config.get_experiment_from_key('test_experiment') + experiment = self.project_config.get_experiment_from_key('test_experiment') - # attributes set to empty dict - with mock.patch('optimizely.helpers.condition.CustomAttributeConditionEvaluator') as custom_attr_eval: - audience.is_user_in_experiment(self.project_config, experiment, {}, self.mock_client_logger) + # attributes set to empty dict + with mock.patch('optimizely.helpers.condition.CustomAttributeConditionEvaluator') as custom_attr_eval: + audience.is_user_in_experiment(self.project_config, experiment, {}, self.mock_client_logger) - self.assertEqual({}, custom_attr_eval.call_args[0][1]) + self.assertEqual({}, custom_attr_eval.call_args[0][1]) - # attributes set to None - with mock.patch('optimizely.helpers.condition.CustomAttributeConditionEvaluator') as custom_attr_eval: - audience.is_user_in_experiment(self.project_config, experiment, None, self.mock_client_logger) + # attributes set to None + with mock.patch('optimizely.helpers.condition.CustomAttributeConditionEvaluator') as custom_attr_eval: + audience.is_user_in_experiment(self.project_config, experiment, None, self.mock_client_logger) - self.assertEqual({}, custom_attr_eval.call_args[0][1]) + self.assertEqual({}, custom_attr_eval.call_args[0][1]) - def test_is_user_in_experiment__returns_True__when_condition_tree_evaluator_returns_True(self): - """ Test that is_user_in_experiment returns True when call to condition_tree_evaluator returns True. """ + def test_is_user_in_experiment__returns_True__when_condition_tree_evaluator_returns_True(self,): + """ Test that is_user_in_experiment returns True when call to condition_tree_evaluator returns True. """ - user_attributes = {'test_attribute': 'test_value_1'} - experiment = self.project_config.get_experiment_from_key('test_experiment') - with mock.patch('optimizely.helpers.condition_tree_evaluator.evaluate', return_value=True): + user_attributes = {'test_attribute': 'test_value_1'} + experiment = self.project_config.get_experiment_from_key('test_experiment') + with mock.patch('optimizely.helpers.condition_tree_evaluator.evaluate', return_value=True): - self.assertStrictTrue(audience.is_user_in_experiment(self.project_config, - experiment, user_attributes, self.mock_client_logger)) + self.assertStrictTrue( + audience.is_user_in_experiment( + self.project_config, experiment, user_attributes, self.mock_client_logger, + ) + ) - def test_is_user_in_experiment__returns_False__when_condition_tree_evaluator_returns_None_or_False(self): - """ Test that is_user_in_experiment returns False when call to condition_tree_evaluator returns None or False. """ + def test_is_user_in_experiment__returns_False__when_condition_tree_evaluator_returns_None_or_False(self,): + """ Test that is_user_in_experiment returns False + when call to condition_tree_evaluator returns None or False. """ - user_attributes = {'test_attribute': 'test_value_1'} - experiment = self.project_config.get_experiment_from_key('test_experiment') - with mock.patch('optimizely.helpers.condition_tree_evaluator.evaluate', return_value=None): + user_attributes = {'test_attribute': 'test_value_1'} + experiment = self.project_config.get_experiment_from_key('test_experiment') + with mock.patch('optimizely.helpers.condition_tree_evaluator.evaluate', return_value=None): - self.assertStrictFalse(audience.is_user_in_experiment( - self.project_config, experiment, user_attributes, self.mock_client_logger)) + self.assertStrictFalse( + audience.is_user_in_experiment( + self.project_config, experiment, user_attributes, self.mock_client_logger, + ) + ) - with mock.patch('optimizely.helpers.condition_tree_evaluator.evaluate', return_value=False): + with mock.patch('optimizely.helpers.condition_tree_evaluator.evaluate', return_value=False): - self.assertStrictFalse(audience.is_user_in_experiment( - self.project_config, experiment, user_attributes, self.mock_client_logger)) + self.assertStrictFalse( + audience.is_user_in_experiment( + self.project_config, experiment, user_attributes, self.mock_client_logger, + ) + ) - def test_is_user_in_experiment__evaluates_audienceIds(self): - """ Test that is_user_in_experiment correctly evaluates audience Ids and + def test_is_user_in_experiment__evaluates_audienceIds(self): + """ Test that is_user_in_experiment correctly evaluates audience Ids and calls custom attribute evaluator for leaf nodes. """ - experiment = self.project_config.get_experiment_from_key('test_experiment') - experiment.audienceIds = ['11154', '11159'] - experiment.audienceConditions = None - - with mock.patch('optimizely.helpers.condition.CustomAttributeConditionEvaluator') as custom_attr_eval: - audience.is_user_in_experiment(self.project_config, experiment, {}, self.mock_client_logger) - - audience_11154 = self.project_config.get_audience('11154') - audience_11159 = self.project_config.get_audience('11159') - custom_attr_eval.assert_has_calls([ - mock.call(audience_11154.conditionList, {}, self.mock_client_logger), - mock.call(audience_11159.conditionList, {}, self.mock_client_logger), - mock.call().evaluate(0), - mock.call().evaluate(0) - ], any_order=True) - - def test_is_user_in_experiment__evaluates_audience_conditions(self): - """ Test that is_user_in_experiment correctly evaluates audienceConditions and + experiment = self.project_config.get_experiment_from_key('test_experiment') + experiment.audienceIds = ['11154', '11159'] + experiment.audienceConditions = None + + with mock.patch('optimizely.helpers.condition.CustomAttributeConditionEvaluator') as custom_attr_eval: + audience.is_user_in_experiment(self.project_config, experiment, {}, self.mock_client_logger) + + audience_11154 = self.project_config.get_audience('11154') + audience_11159 = self.project_config.get_audience('11159') + custom_attr_eval.assert_has_calls( + [ + mock.call(audience_11154.conditionList, {}, self.mock_client_logger), + mock.call(audience_11159.conditionList, {}, self.mock_client_logger), + mock.call().evaluate(0), + mock.call().evaluate(0), + ], + any_order=True, + ) + + def test_is_user_in_experiment__evaluates_audience_conditions(self): + """ Test that is_user_in_experiment correctly evaluates audienceConditions and calls custom attribute evaluator for leaf nodes. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - project_config = opt_obj.config_manager.get_config() - experiment = project_config.get_experiment_from_key('audience_combinations_experiment') - experiment.audienceIds = [] - experiment.audienceConditions = ['or', ['or', '3468206642', '3988293898'], ['or', '3988293899', '3468206646', ]] - - with mock.patch('optimizely.helpers.condition.CustomAttributeConditionEvaluator') as custom_attr_eval: - audience.is_user_in_experiment(project_config, experiment, {}, self.mock_client_logger) - - audience_3468206642 = project_config.get_audience('3468206642') - audience_3988293898 = project_config.get_audience('3988293898') - audience_3988293899 = project_config.get_audience('3988293899') - audience_3468206646 = project_config.get_audience('3468206646') - - custom_attr_eval.assert_has_calls([ - mock.call(audience_3468206642.conditionList, {}, self.mock_client_logger), - mock.call(audience_3988293898.conditionList, {}, self.mock_client_logger), - mock.call(audience_3988293899.conditionList, {}, self.mock_client_logger), - mock.call(audience_3468206646.conditionList, {}, self.mock_client_logger), - mock.call().evaluate(0), - mock.call().evaluate(0), - mock.call().evaluate(0), - mock.call().evaluate(0) - ], any_order=True) - - def test_is_user_in_experiment__evaluates_audience_conditions_leaf_node(self): - """ Test that is_user_in_experiment correctly evaluates leaf node in audienceConditions. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - project_config = opt_obj.config_manager.get_config() - experiment = project_config.get_experiment_from_key('audience_combinations_experiment') - experiment.audienceConditions = '3468206645' - - with mock.patch('optimizely.helpers.condition.CustomAttributeConditionEvaluator') as custom_attr_eval: - audience.is_user_in_experiment(project_config, experiment, {}, self.mock_client_logger) - - audience_3468206645 = project_config.get_audience('3468206645') - - custom_attr_eval.assert_has_calls([ - mock.call(audience_3468206645.conditionList, {}, self.mock_client_logger), - mock.call().evaluate(0), - mock.call().evaluate(1), - ], any_order=True) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) + project_config = opt_obj.config_manager.get_config() + experiment = project_config.get_experiment_from_key('audience_combinations_experiment') + experiment.audienceIds = [] + experiment.audienceConditions = [ + 'or', + ['or', '3468206642', '3988293898'], + ['or', '3988293899', '3468206646'], + ] + + with mock.patch('optimizely.helpers.condition.CustomAttributeConditionEvaluator') as custom_attr_eval: + audience.is_user_in_experiment(project_config, experiment, {}, self.mock_client_logger) + + audience_3468206642 = project_config.get_audience('3468206642') + audience_3988293898 = project_config.get_audience('3988293898') + audience_3988293899 = project_config.get_audience('3988293899') + audience_3468206646 = project_config.get_audience('3468206646') + + custom_attr_eval.assert_has_calls( + [ + mock.call(audience_3468206642.conditionList, {}, self.mock_client_logger), + mock.call(audience_3988293898.conditionList, {}, self.mock_client_logger), + mock.call(audience_3988293899.conditionList, {}, self.mock_client_logger), + mock.call(audience_3468206646.conditionList, {}, self.mock_client_logger), + mock.call().evaluate(0), + mock.call().evaluate(0), + mock.call().evaluate(0), + mock.call().evaluate(0), + ], + any_order=True, + ) + + def test_is_user_in_experiment__evaluates_audience_conditions_leaf_node(self): + """ Test that is_user_in_experiment correctly evaluates leaf node in audienceConditions. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) + project_config = opt_obj.config_manager.get_config() + experiment = project_config.get_experiment_from_key('audience_combinations_experiment') + experiment.audienceConditions = '3468206645' + + with mock.patch('optimizely.helpers.condition.CustomAttributeConditionEvaluator') as custom_attr_eval: + audience.is_user_in_experiment(project_config, experiment, {}, self.mock_client_logger) + + audience_3468206645 = project_config.get_audience('3468206645') + + custom_attr_eval.assert_has_calls( + [ + mock.call(audience_3468206645.conditionList, {}, self.mock_client_logger), + mock.call().evaluate(0), + mock.call().evaluate(1), + ], + any_order=True, + ) class AudienceLoggingTest(base.BaseTest): - - def setUp(self): - base.BaseTest.setUp(self) - self.mock_client_logger = mock.MagicMock() - - def test_is_user_in_experiment__with_no_audience(self): - experiment = self.project_config.get_experiment_from_key('test_experiment') - experiment.audienceIds = [] - experiment.audienceConditions = [] - - audience.is_user_in_experiment(self.project_config, experiment, {}, self.mock_client_logger) - - self.mock_client_logger.assert_has_calls([ - mock.call.debug('Evaluating audiences for experiment "test_experiment": [].'), - mock.call.info('Audiences for experiment "test_experiment" collectively evaluated to TRUE.') - ]) - - def test_is_user_in_experiment__evaluates_audienceIds(self): - user_attributes = {'test_attribute': 'test_value_1'} - experiment = self.project_config.get_experiment_from_key('test_experiment') - experiment.audienceIds = ['11154', '11159'] - experiment.audienceConditions = None - audience_11154 = self.project_config.get_audience('11154') - audience_11159 = self.project_config.get_audience('11159') - - with mock.patch('optimizely.helpers.condition.CustomAttributeConditionEvaluator.evaluate', - side_effect=[None, None]): - audience.is_user_in_experiment(self.project_config, experiment, user_attributes, self.mock_client_logger) - - self.assertEqual(3, self.mock_client_logger.debug.call_count) - self.assertEqual(3, self.mock_client_logger.info.call_count) - - self.mock_client_logger.assert_has_calls([ - mock.call.debug('Evaluating audiences for experiment "test_experiment": ["11154", "11159"].'), - mock.call.debug('Starting to evaluate audience "11154" with conditions: ' + audience_11154.conditions + '.'), - mock.call.info('Audience "11154" evaluated to UNKNOWN.'), - mock.call.debug('Starting to evaluate audience "11159" with conditions: ' + audience_11159.conditions + '.'), - mock.call.info('Audience "11159" evaluated to UNKNOWN.'), - mock.call.info('Audiences for experiment "test_experiment" collectively evaluated to FALSE.') - ]) - - def test_is_user_in_experiment__evaluates_audience_conditions(self): - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - project_config = opt_obj.config_manager.get_config() - experiment = project_config.get_experiment_from_key('audience_combinations_experiment') - experiment.audienceIds = [] - experiment.audienceConditions = ['or', ['or', '3468206642', '3988293898', '3988293899']] - audience_3468206642 = project_config.get_audience('3468206642') - audience_3988293898 = project_config.get_audience('3988293898') - audience_3988293899 = project_config.get_audience('3988293899') - - with mock.patch('optimizely.helpers.condition.CustomAttributeConditionEvaluator.evaluate', - side_effect=[False, None, True]): - audience.is_user_in_experiment(project_config, experiment, {}, self.mock_client_logger) - - self.assertEqual(4, self.mock_client_logger.debug.call_count) - self.assertEqual(4, self.mock_client_logger.info.call_count) - - self.mock_client_logger.assert_has_calls([ - mock.call.debug( - 'Evaluating audiences for experiment "audience_combinations_experiment": ["or", ["or", "3468206642", ' - '"3988293898", "3988293899"]].' - ), - mock.call.debug('Starting to evaluate audience "3468206642" with ' - 'conditions: ' + audience_3468206642.conditions + '.'), - mock.call.info('Audience "3468206642" evaluated to FALSE.'), - mock.call.debug('Starting to evaluate audience "3988293898" with ' - 'conditions: ' + audience_3988293898.conditions + '.'), - mock.call.info('Audience "3988293898" evaluated to UNKNOWN.'), - mock.call.debug('Starting to evaluate audience "3988293899" with ' - 'conditions: ' + audience_3988293899.conditions + '.'), - mock.call.info('Audience "3988293899" evaluated to TRUE.'), - mock.call.info('Audiences for experiment "audience_combinations_experiment" collectively evaluated to TRUE.') - ]) + def setUp(self): + base.BaseTest.setUp(self) + self.mock_client_logger = mock.MagicMock() + + def test_is_user_in_experiment__with_no_audience(self): + experiment = self.project_config.get_experiment_from_key('test_experiment') + experiment.audienceIds = [] + experiment.audienceConditions = [] + + audience.is_user_in_experiment(self.project_config, experiment, {}, self.mock_client_logger) + + self.mock_client_logger.assert_has_calls( + [ + mock.call.debug('Evaluating audiences for experiment "test_experiment": [].'), + mock.call.info('Audiences for experiment "test_experiment" collectively evaluated to TRUE.'), + ] + ) + + def test_is_user_in_experiment__evaluates_audienceIds(self): + user_attributes = {'test_attribute': 'test_value_1'} + experiment = self.project_config.get_experiment_from_key('test_experiment') + experiment.audienceIds = ['11154', '11159'] + experiment.audienceConditions = None + audience_11154 = self.project_config.get_audience('11154') + audience_11159 = self.project_config.get_audience('11159') + + with mock.patch( + 'optimizely.helpers.condition.CustomAttributeConditionEvaluator.evaluate', side_effect=[None, None], + ): + audience.is_user_in_experiment( + self.project_config, experiment, user_attributes, self.mock_client_logger, + ) + + self.assertEqual(3, self.mock_client_logger.debug.call_count) + self.assertEqual(3, self.mock_client_logger.info.call_count) + + self.mock_client_logger.assert_has_calls( + [ + mock.call.debug('Evaluating audiences for experiment "test_experiment": ["11154", "11159"].'), + mock.call.debug( + 'Starting to evaluate audience "11154" with conditions: ' + audience_11154.conditions + '.' + ), + mock.call.info('Audience "11154" evaluated to UNKNOWN.'), + mock.call.debug( + 'Starting to evaluate audience "11159" with conditions: ' + audience_11159.conditions + '.' + ), + mock.call.info('Audience "11159" evaluated to UNKNOWN.'), + mock.call.info('Audiences for experiment "test_experiment" collectively evaluated to FALSE.'), + ] + ) + + def test_is_user_in_experiment__evaluates_audience_conditions(self): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) + project_config = opt_obj.config_manager.get_config() + experiment = project_config.get_experiment_from_key('audience_combinations_experiment') + experiment.audienceIds = [] + experiment.audienceConditions = [ + 'or', + ['or', '3468206642', '3988293898', '3988293899'], + ] + audience_3468206642 = project_config.get_audience('3468206642') + audience_3988293898 = project_config.get_audience('3988293898') + audience_3988293899 = project_config.get_audience('3988293899') + + with mock.patch( + 'optimizely.helpers.condition.CustomAttributeConditionEvaluator.evaluate', side_effect=[False, None, True], + ): + audience.is_user_in_experiment(project_config, experiment, {}, self.mock_client_logger) + + self.assertEqual(4, self.mock_client_logger.debug.call_count) + self.assertEqual(4, self.mock_client_logger.info.call_count) + + self.mock_client_logger.assert_has_calls( + [ + mock.call.debug( + 'Evaluating audiences for experiment ' + '"audience_combinations_experiment": ["or", ["or", "3468206642", ' + '"3988293898", "3988293899"]].' + ), + mock.call.debug( + 'Starting to evaluate audience "3468206642" with ' + 'conditions: ' + audience_3468206642.conditions + '.' + ), + mock.call.info('Audience "3468206642" evaluated to FALSE.'), + mock.call.debug( + 'Starting to evaluate audience "3988293898" with ' + 'conditions: ' + audience_3988293898.conditions + '.' + ), + mock.call.info('Audience "3988293898" evaluated to UNKNOWN.'), + mock.call.debug( + 'Starting to evaluate audience "3988293899" with ' + 'conditions: ' + audience_3988293899.conditions + '.' + ), + mock.call.info('Audience "3988293899" evaluated to TRUE.'), + mock.call.info( + 'Audiences for experiment "audience_combinations_experiment" collectively evaluated to TRUE.' + ), + ] + ) diff --git a/tests/helpers_tests/test_condition.py b/tests/helpers_tests/test_condition.py index e7bd5fc6..b4dee368 100644 --- a/tests/helpers_tests/test_condition.py +++ b/tests/helpers_tests/test_condition.py @@ -37,1259 +37,1333 @@ class CustomAttributeConditionEvaluator(base.BaseTest): + def setUp(self): + base.BaseTest.setUp(self) + self.condition_list = [ + browserConditionSafari, + booleanCondition, + integerCondition, + doubleCondition, + ] + self.mock_client_logger = mock.MagicMock() - def setUp(self): - base.BaseTest.setUp(self) - self.condition_list = [browserConditionSafari, booleanCondition, integerCondition, doubleCondition] - self.mock_client_logger = mock.MagicMock() + def test_evaluate__returns_true__when_attributes_pass_audience_condition(self): + evaluator = condition_helper.CustomAttributeConditionEvaluator( + self.condition_list, {'browser_type': 'safari'}, self.mock_client_logger + ) - def test_evaluate__returns_true__when_attributes_pass_audience_condition(self): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - self.condition_list, {'browser_type': 'safari'}, self.mock_client_logger - ) + self.assertStrictTrue(evaluator.evaluate(0)) - self.assertStrictTrue(evaluator.evaluate(0)) + def test_evaluate__returns_false__when_attributes_fail_audience_condition(self): + evaluator = condition_helper.CustomAttributeConditionEvaluator( + self.condition_list, {'browser_type': 'chrome'}, self.mock_client_logger + ) - def test_evaluate__returns_false__when_attributes_fail_audience_condition(self): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - self.condition_list, {'browser_type': 'chrome'}, self.mock_client_logger - ) + self.assertStrictFalse(evaluator.evaluate(0)) - self.assertStrictFalse(evaluator.evaluate(0)) + def test_evaluate__evaluates__different_typed_attributes(self): + userAttributes = { + 'browser_type': 'safari', + 'is_firefox': True, + 'num_users': 10, + 'pi_value': 3.14, + } - def test_evaluate__evaluates__different_typed_attributes(self): - userAttributes = { - 'browser_type': 'safari', - 'is_firefox': True, - 'num_users': 10, - 'pi_value': 3.14, - } + evaluator = condition_helper.CustomAttributeConditionEvaluator( + self.condition_list, userAttributes, self.mock_client_logger + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - self.condition_list, userAttributes, self.mock_client_logger - ) + self.assertStrictTrue(evaluator.evaluate(0)) + self.assertStrictTrue(evaluator.evaluate(1)) + self.assertStrictTrue(evaluator.evaluate(2)) + self.assertStrictTrue(evaluator.evaluate(3)) - self.assertStrictTrue(evaluator.evaluate(0)) - self.assertStrictTrue(evaluator.evaluate(1)) - self.assertStrictTrue(evaluator.evaluate(2)) - self.assertStrictTrue(evaluator.evaluate(3)) + def test_evaluate__returns_null__when_condition_has_an_invalid_match_property(self): - def test_evaluate__returns_null__when_condition_has_an_invalid_match_property(self): + condition_list = [['weird_condition', 'hi', 'custom_attribute', 'weird_match']] - condition_list = [['weird_condition', 'hi', 'custom_attribute', 'weird_match']] + evaluator = condition_helper.CustomAttributeConditionEvaluator( + condition_list, {'weird_condition': 'hi'}, self.mock_client_logger + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - condition_list, {'weird_condition': 'hi'}, self.mock_client_logger - ) + self.assertIsNone(evaluator.evaluate(0)) - self.assertIsNone(evaluator.evaluate(0)) + def test_evaluate__assumes_exact__when_condition_match_property_is_none(self): - def test_evaluate__assumes_exact__when_condition_match_property_is_none(self): + condition_list = [['favorite_constellation', 'Lacerta', 'custom_attribute', None]] - condition_list = [['favorite_constellation', 'Lacerta', 'custom_attribute', None]] + evaluator = condition_helper.CustomAttributeConditionEvaluator( + condition_list, {'favorite_constellation': 'Lacerta'}, self.mock_client_logger, + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - condition_list, {'favorite_constellation': 'Lacerta'}, self.mock_client_logger - ) + self.assertStrictTrue(evaluator.evaluate(0)) - self.assertStrictTrue(evaluator.evaluate(0)) + def test_evaluate__returns_null__when_condition_has_an_invalid_type_property(self): - def test_evaluate__returns_null__when_condition_has_an_invalid_type_property(self): + condition_list = [['weird_condition', 'hi', 'weird_type', 'exact']] - condition_list = [['weird_condition', 'hi', 'weird_type', 'exact']] + evaluator = condition_helper.CustomAttributeConditionEvaluator( + condition_list, {'weird_condition': 'hi'}, self.mock_client_logger + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - condition_list, {'weird_condition': 'hi'}, self.mock_client_logger - ) + self.assertIsNone(evaluator.evaluate(0)) - self.assertIsNone(evaluator.evaluate(0)) + def test_exists__returns_false__when_no_user_provided_value(self): - def test_exists__returns_false__when_no_user_provided_value(self): + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exists_condition_list, {}, self.mock_client_logger + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exists_condition_list, {}, self.mock_client_logger - ) + self.assertStrictFalse(evaluator.evaluate(0)) - self.assertStrictFalse(evaluator.evaluate(0)) + def test_exists__returns_false__when_user_provided_value_is_null(self): - def test_exists__returns_false__when_user_provided_value_is_null(self): + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exists_condition_list, {'input_value': None}, self.mock_client_logger + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exists_condition_list, {'input_value': None}, self.mock_client_logger - ) + self.assertStrictFalse(evaluator.evaluate(0)) - self.assertStrictFalse(evaluator.evaluate(0)) + def test_exists__returns_true__when_user_provided_value_is_string(self): - def test_exists__returns_true__when_user_provided_value_is_string(self): + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exists_condition_list, {'input_value': 'hi'}, self.mock_client_logger + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exists_condition_list, {'input_value': 'hi'}, self.mock_client_logger - ) + self.assertStrictTrue(evaluator.evaluate(0)) - self.assertStrictTrue(evaluator.evaluate(0)) + def test_exists__returns_true__when_user_provided_value_is_number(self): - def test_exists__returns_true__when_user_provided_value_is_number(self): + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exists_condition_list, {'input_value': 10}, self.mock_client_logger + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exists_condition_list, {'input_value': 10}, self.mock_client_logger - ) + self.assertStrictTrue(evaluator.evaluate(0)) - self.assertStrictTrue(evaluator.evaluate(0)) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exists_condition_list, {'input_value': 10.0}, self.mock_client_logger + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exists_condition_list, {'input_value': 10.0}, self.mock_client_logger - ) + self.assertStrictTrue(evaluator.evaluate(0)) - self.assertStrictTrue(evaluator.evaluate(0)) + def test_exists__returns_true__when_user_provided_value_is_boolean(self): - def test_exists__returns_true__when_user_provided_value_is_boolean(self): + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exists_condition_list, {'input_value': False}, self.mock_client_logger + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exists_condition_list, {'input_value': False}, self.mock_client_logger - ) + self.assertStrictTrue(evaluator.evaluate(0)) - self.assertStrictTrue(evaluator.evaluate(0)) + def test_exact_string__returns_true__when_user_provided_value_is_equal_to_condition_value(self,): - def test_exact_string__returns_true__when_user_provided_value_is_equal_to_condition_value(self): + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_string_condition_list, {'favorite_constellation': 'Lacerta'}, self.mock_client_logger, + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_string_condition_list, {'favorite_constellation': 'Lacerta'}, self.mock_client_logger - ) + self.assertStrictTrue(evaluator.evaluate(0)) - self.assertStrictTrue(evaluator.evaluate(0)) + def test_exact_string__returns_false__when_user_provided_value_is_not_equal_to_condition_value(self,): - def test_exact_string__returns_false__when_user_provided_value_is_not_equal_to_condition_value(self): + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_string_condition_list, {'favorite_constellation': 'The Big Dipper'}, self.mock_client_logger, + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_string_condition_list, {'favorite_constellation': 'The Big Dipper'}, self.mock_client_logger - ) + self.assertStrictFalse(evaluator.evaluate(0)) - self.assertStrictFalse(evaluator.evaluate(0)) + def test_exact_string__returns_null__when_user_provided_value_is_different_type_from_condition_value(self,): - def test_exact_string__returns_null__when_user_provided_value_is_different_type_from_condition_value(self): + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_string_condition_list, {'favorite_constellation': False}, self.mock_client_logger, + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_string_condition_list, {'favorite_constellation': False}, self.mock_client_logger - ) + self.assertIsNone(evaluator.evaluate(0)) - self.assertIsNone(evaluator.evaluate(0)) + def test_exact_string__returns_null__when_no_user_provided_value(self): - def test_exact_string__returns_null__when_no_user_provided_value(self): + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_string_condition_list, {}, self.mock_client_logger + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_string_condition_list, {}, self.mock_client_logger - ) + self.assertIsNone(evaluator.evaluate(0)) - self.assertIsNone(evaluator.evaluate(0)) + def test_exact_int__returns_true__when_user_provided_value_is_equal_to_condition_value(self,): - def test_exact_int__returns_true__when_user_provided_value_is_equal_to_condition_value(self): + if PY2: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_int_condition_list, {'lasers_count': long(9000)}, self.mock_client_logger, + ) - if PY2: - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_int_condition_list, {'lasers_count': long(9000)}, self.mock_client_logger - ) + self.assertStrictTrue(evaluator.evaluate(0)) - self.assertStrictTrue(evaluator.evaluate(0)) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_int_condition_list, {'lasers_count': 9000}, self.mock_client_logger + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_int_condition_list, {'lasers_count': 9000}, self.mock_client_logger - ) + self.assertStrictTrue(evaluator.evaluate(0)) - self.assertStrictTrue(evaluator.evaluate(0)) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_int_condition_list, {'lasers_count': 9000.0}, self.mock_client_logger + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_int_condition_list, {'lasers_count': 9000.0}, self.mock_client_logger - ) + self.assertStrictTrue(evaluator.evaluate(0)) - self.assertStrictTrue(evaluator.evaluate(0)) + def test_exact_float__returns_true__when_user_provided_value_is_equal_to_condition_value(self,): - def test_exact_float__returns_true__when_user_provided_value_is_equal_to_condition_value(self): + if PY2: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_float_condition_list, {'lasers_count': long(9000)}, self.mock_client_logger, + ) - if PY2: - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_float_condition_list, {'lasers_count': long(9000)}, self.mock_client_logger - ) + self.assertStrictTrue(evaluator.evaluate(0)) - self.assertStrictTrue(evaluator.evaluate(0)) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_float_condition_list, {'lasers_count': 9000}, self.mock_client_logger + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_float_condition_list, {'lasers_count': 9000}, self.mock_client_logger - ) + self.assertStrictTrue(evaluator.evaluate(0)) - self.assertStrictTrue(evaluator.evaluate(0)) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_float_condition_list, {'lasers_count': 9000.0}, self.mock_client_logger, + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_float_condition_list, {'lasers_count': 9000.0}, self.mock_client_logger - ) + self.assertStrictTrue(evaluator.evaluate(0)) - self.assertStrictTrue(evaluator.evaluate(0)) + def test_exact_int__returns_false__when_user_provided_value_is_not_equal_to_condition_value(self,): - def test_exact_int__returns_false__when_user_provided_value_is_not_equal_to_condition_value(self): + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_int_condition_list, {'lasers_count': 8000}, self.mock_client_logger + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_int_condition_list, {'lasers_count': 8000}, self.mock_client_logger - ) + self.assertStrictFalse(evaluator.evaluate(0)) - self.assertStrictFalse(evaluator.evaluate(0)) + def test_exact_float__returns_false__when_user_provided_value_is_not_equal_to_condition_value(self,): - def test_exact_float__returns_false__when_user_provided_value_is_not_equal_to_condition_value(self): + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_float_condition_list, {'lasers_count': 8000.0}, self.mock_client_logger, + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_float_condition_list, {'lasers_count': 8000.0}, self.mock_client_logger - ) + self.assertStrictFalse(evaluator.evaluate(0)) - self.assertStrictFalse(evaluator.evaluate(0)) + def test_exact_int__returns_null__when_user_provided_value_is_different_type_from_condition_value(self,): - def test_exact_int__returns_null__when_user_provided_value_is_different_type_from_condition_value(self): + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_int_condition_list, {'lasers_count': 'hi'}, self.mock_client_logger + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_int_condition_list, {'lasers_count': 'hi'}, self.mock_client_logger - ) + self.assertIsNone(evaluator.evaluate(0)) - self.assertIsNone(evaluator.evaluate(0)) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_int_condition_list, {'lasers_count': True}, self.mock_client_logger + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_int_condition_list, {'lasers_count': True}, self.mock_client_logger - ) + self.assertIsNone(evaluator.evaluate(0)) - self.assertIsNone(evaluator.evaluate(0)) + def test_exact_float__returns_null__when_user_provided_value_is_different_type_from_condition_value(self,): - def test_exact_float__returns_null__when_user_provided_value_is_different_type_from_condition_value(self): + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_float_condition_list, {'lasers_count': 'hi'}, self.mock_client_logger + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_float_condition_list, {'lasers_count': 'hi'}, self.mock_client_logger - ) + self.assertIsNone(evaluator.evaluate(0)) - self.assertIsNone(evaluator.evaluate(0)) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_float_condition_list, {'lasers_count': True}, self.mock_client_logger + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_float_condition_list, {'lasers_count': True}, self.mock_client_logger - ) + self.assertIsNone(evaluator.evaluate(0)) - self.assertIsNone(evaluator.evaluate(0)) + def test_exact_int__returns_null__when_no_user_provided_value(self): - def test_exact_int__returns_null__when_no_user_provided_value(self): + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_int_condition_list, {}, self.mock_client_logger + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_int_condition_list, {}, self.mock_client_logger - ) + self.assertIsNone(evaluator.evaluate(0)) - self.assertIsNone(evaluator.evaluate(0)) + def test_exact_float__returns_null__when_no_user_provided_value(self): - def test_exact_float__returns_null__when_no_user_provided_value(self): + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_float_condition_list, {}, self.mock_client_logger + ) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_float_condition_list, {}, self.mock_client_logger - ) + self.assertIsNone(evaluator.evaluate(0)) - self.assertIsNone(evaluator.evaluate(0)) - - def test_exact__given_number_values__calls_is_finite_number(self): - """ Test that CustomAttributeConditionEvaluator.evaluate returns True + def test_exact__given_number_values__calls_is_finite_number(self): + """ Test that CustomAttributeConditionEvaluator.evaluate returns True if is_finite_number returns True. Returns None if is_finite_number returns False. """ - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_int_condition_list, {'lasers_count': 9000}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_int_condition_list, {'lasers_count': 9000}, self.mock_client_logger + ) - # assert that isFiniteNumber only needs to reject condition value to stop evaluation. - with mock.patch('optimizely.helpers.validator.is_finite_number', - side_effect=[False, True]) as mock_is_finite: - self.assertIsNone(evaluator.evaluate(0)) + # assert that isFiniteNumber only needs to reject condition value to stop evaluation. + with mock.patch('optimizely.helpers.validator.is_finite_number', side_effect=[False, True]) as mock_is_finite: + self.assertIsNone(evaluator.evaluate(0)) - mock_is_finite.assert_called_once_with(9000) + mock_is_finite.assert_called_once_with(9000) - # assert that isFiniteNumber evaluates user value only if it has accepted condition value. - with mock.patch('optimizely.helpers.validator.is_finite_number', - side_effect=[True, False]) as mock_is_finite: - self.assertIsNone(evaluator.evaluate(0)) + # assert that isFiniteNumber evaluates user value only if it has accepted condition value. + with mock.patch('optimizely.helpers.validator.is_finite_number', side_effect=[True, False]) as mock_is_finite: + self.assertIsNone(evaluator.evaluate(0)) - mock_is_finite.assert_has_calls([mock.call(9000), mock.call(9000)]) + mock_is_finite.assert_has_calls([mock.call(9000), mock.call(9000)]) - # assert CustomAttributeConditionEvaluator.evaluate returns True only when isFiniteNumber returns - # True both for condition and user values. - with mock.patch('optimizely.helpers.validator.is_finite_number', - side_effect=[True, True]) as mock_is_finite: - self.assertTrue(evaluator.evaluate(0)) + # assert CustomAttributeConditionEvaluator.evaluate returns True only when isFiniteNumber returns + # True both for condition and user values. + with mock.patch('optimizely.helpers.validator.is_finite_number', side_effect=[True, True]) as mock_is_finite: + self.assertTrue(evaluator.evaluate(0)) - mock_is_finite.assert_has_calls([mock.call(9000), mock.call(9000)]) + mock_is_finite.assert_has_calls([mock.call(9000), mock.call(9000)]) - def test_exact_bool__returns_true__when_user_provided_value_is_equal_to_condition_value(self): + def test_exact_bool__returns_true__when_user_provided_value_is_equal_to_condition_value(self,): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_bool_condition_list, {'did_register_user': False}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_bool_condition_list, {'did_register_user': False}, self.mock_client_logger, + ) - self.assertStrictTrue(evaluator.evaluate(0)) + self.assertStrictTrue(evaluator.evaluate(0)) - def test_exact_bool__returns_false__when_user_provided_value_is_not_equal_to_condition_value(self): + def test_exact_bool__returns_false__when_user_provided_value_is_not_equal_to_condition_value(self,): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_bool_condition_list, {'did_register_user': True}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_bool_condition_list, {'did_register_user': True}, self.mock_client_logger, + ) - self.assertStrictFalse(evaluator.evaluate(0)) + self.assertStrictFalse(evaluator.evaluate(0)) - def test_exact_bool__returns_null__when_user_provided_value_is_different_type_from_condition_value(self): + def test_exact_bool__returns_null__when_user_provided_value_is_different_type_from_condition_value(self,): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_bool_condition_list, {'did_register_user': 0}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_bool_condition_list, {'did_register_user': 0}, self.mock_client_logger + ) - self.assertIsNone(evaluator.evaluate(0)) + self.assertIsNone(evaluator.evaluate(0)) - def test_exact_bool__returns_null__when_no_user_provided_value(self): + def test_exact_bool__returns_null__when_no_user_provided_value(self): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_bool_condition_list, {}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_bool_condition_list, {}, self.mock_client_logger + ) - self.assertIsNone(evaluator.evaluate(0)) + self.assertIsNone(evaluator.evaluate(0)) - def test_substring__returns_true__when_condition_value_is_substring_of_user_value(self): + def test_substring__returns_true__when_condition_value_is_substring_of_user_value(self,): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - substring_condition_list, {'headline_text': 'Limited time, buy now!'}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + substring_condition_list, {'headline_text': 'Limited time, buy now!'}, self.mock_client_logger, + ) - self.assertStrictTrue(evaluator.evaluate(0)) + self.assertStrictTrue(evaluator.evaluate(0)) - def test_substring__returns_false__when_condition_value_is_not_a_substring_of_user_value(self): + def test_substring__returns_false__when_condition_value_is_not_a_substring_of_user_value(self,): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - substring_condition_list, {'headline_text': 'Breaking news!'}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + substring_condition_list, {'headline_text': 'Breaking news!'}, self.mock_client_logger, + ) - self.assertStrictFalse(evaluator.evaluate(0)) + self.assertStrictFalse(evaluator.evaluate(0)) - def test_substring__returns_null__when_user_provided_value_not_a_string(self): + def test_substring__returns_null__when_user_provided_value_not_a_string(self): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - substring_condition_list, {'headline_text': 10}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + substring_condition_list, {'headline_text': 10}, self.mock_client_logger + ) - self.assertIsNone(evaluator.evaluate(0)) + self.assertIsNone(evaluator.evaluate(0)) - def test_substring__returns_null__when_no_user_provided_value(self): + def test_substring__returns_null__when_no_user_provided_value(self): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - substring_condition_list, {}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + substring_condition_list, {}, self.mock_client_logger + ) - self.assertIsNone(evaluator.evaluate(0)) + self.assertIsNone(evaluator.evaluate(0)) - def test_greater_than_int__returns_true__when_user_value_greater_than_condition_value(self): + def test_greater_than_int__returns_true__when_user_value_greater_than_condition_value(self,): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_int_condition_list, {'meters_travelled': 48.1}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_int_condition_list, {'meters_travelled': 48.1}, self.mock_client_logger + ) - self.assertStrictTrue(evaluator.evaluate(0)) + self.assertStrictTrue(evaluator.evaluate(0)) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_int_condition_list, {'meters_travelled': 49}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_int_condition_list, {'meters_travelled': 49}, self.mock_client_logger + ) - self.assertStrictTrue(evaluator.evaluate(0)) + self.assertStrictTrue(evaluator.evaluate(0)) - if PY2: - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_int_condition_list, {'meters_travelled': long(49)}, self.mock_client_logger - ) + if PY2: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_int_condition_list, {'meters_travelled': long(49)}, self.mock_client_logger, + ) - self.assertStrictTrue(evaluator.evaluate(0)) + self.assertStrictTrue(evaluator.evaluate(0)) - def test_greater_than_float__returns_true__when_user_value_greater_than_condition_value(self): + def test_greater_than_float__returns_true__when_user_value_greater_than_condition_value(self,): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_float_condition_list, {'meters_travelled': 48.3}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_float_condition_list, {'meters_travelled': 48.3}, self.mock_client_logger + ) - self.assertStrictTrue(evaluator.evaluate(0)) + self.assertStrictTrue(evaluator.evaluate(0)) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_float_condition_list, {'meters_travelled': 49}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_float_condition_list, {'meters_travelled': 49}, self.mock_client_logger + ) - self.assertStrictTrue(evaluator.evaluate(0)) + self.assertStrictTrue(evaluator.evaluate(0)) - if PY2: - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_float_condition_list, {'meters_travelled': long(49)}, self.mock_client_logger - ) + if PY2: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_float_condition_list, {'meters_travelled': long(49)}, self.mock_client_logger, + ) - self.assertStrictTrue(evaluator.evaluate(0)) + self.assertStrictTrue(evaluator.evaluate(0)) - def test_greater_than_int__returns_false__when_user_value_not_greater_than_condition_value(self): + def test_greater_than_int__returns_false__when_user_value_not_greater_than_condition_value(self,): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_int_condition_list, {'meters_travelled': 47.9}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_int_condition_list, {'meters_travelled': 47.9}, self.mock_client_logger + ) - self.assertStrictFalse(evaluator.evaluate(0)) + self.assertStrictFalse(evaluator.evaluate(0)) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_int_condition_list, {'meters_travelled': 47}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_int_condition_list, {'meters_travelled': 47}, self.mock_client_logger + ) - self.assertStrictFalse(evaluator.evaluate(0)) + self.assertStrictFalse(evaluator.evaluate(0)) - if PY2: - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_int_condition_list, {'meters_travelled': long(47)}, self.mock_client_logger - ) + if PY2: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_int_condition_list, {'meters_travelled': long(47)}, self.mock_client_logger, + ) - self.assertStrictFalse(evaluator.evaluate(0)) + self.assertStrictFalse(evaluator.evaluate(0)) - def test_greater_than_float__returns_false__when_user_value_not_greater_than_condition_value(self): + def test_greater_than_float__returns_false__when_user_value_not_greater_than_condition_value(self,): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_float_condition_list, {'meters_travelled': 48.2}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_float_condition_list, {'meters_travelled': 48.2}, self.mock_client_logger + ) - self.assertStrictFalse(evaluator.evaluate(0)) + self.assertStrictFalse(evaluator.evaluate(0)) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_float_condition_list, {'meters_travelled': 48}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_float_condition_list, {'meters_travelled': 48}, self.mock_client_logger + ) - self.assertStrictFalse(evaluator.evaluate(0)) + self.assertStrictFalse(evaluator.evaluate(0)) - if PY2: - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_float_condition_list, {'meters_travelled': long(48)}, self.mock_client_logger - ) + if PY2: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_float_condition_list, {'meters_travelled': long(48)}, self.mock_client_logger, + ) - self.assertStrictFalse(evaluator.evaluate(0)) + self.assertStrictFalse(evaluator.evaluate(0)) - def test_greater_than_int__returns_null__when_user_value_is_not_a_number(self): + def test_greater_than_int__returns_null__when_user_value_is_not_a_number(self): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_int_condition_list, {'meters_travelled': 'a long way'}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_int_condition_list, {'meters_travelled': 'a long way'}, self.mock_client_logger, + ) - self.assertIsNone(evaluator.evaluate(0)) + self.assertIsNone(evaluator.evaluate(0)) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_int_condition_list, {'meters_travelled': False}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_int_condition_list, {'meters_travelled': False}, self.mock_client_logger + ) - self.assertIsNone(evaluator.evaluate(0)) + self.assertIsNone(evaluator.evaluate(0)) - def test_greater_than_float__returns_null__when_user_value_is_not_a_number(self): + def test_greater_than_float__returns_null__when_user_value_is_not_a_number(self): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_float_condition_list, {'meters_travelled': 'a long way'}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_float_condition_list, {'meters_travelled': 'a long way'}, self.mock_client_logger, + ) - self.assertIsNone(evaluator.evaluate(0)) + self.assertIsNone(evaluator.evaluate(0)) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_float_condition_list, {'meters_travelled': False}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_float_condition_list, {'meters_travelled': False}, self.mock_client_logger, + ) - self.assertIsNone(evaluator.evaluate(0)) + self.assertIsNone(evaluator.evaluate(0)) - def test_greater_than_int__returns_null__when_no_user_provided_value(self): + def test_greater_than_int__returns_null__when_no_user_provided_value(self): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_int_condition_list, {}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_int_condition_list, {}, self.mock_client_logger + ) - self.assertIsNone(evaluator.evaluate(0)) + self.assertIsNone(evaluator.evaluate(0)) - def test_greater_than_float__returns_null__when_no_user_provided_value(self): + def test_greater_than_float__returns_null__when_no_user_provided_value(self): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_float_condition_list, {}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_float_condition_list, {}, self.mock_client_logger + ) - self.assertIsNone(evaluator.evaluate(0)) + self.assertIsNone(evaluator.evaluate(0)) - def test_less_than_int__returns_true__when_user_value_less_than_condition_value(self): + def test_less_than_int__returns_true__when_user_value_less_than_condition_value(self,): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - lt_int_condition_list, {'meters_travelled': 47.9}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_int_condition_list, {'meters_travelled': 47.9}, self.mock_client_logger + ) - self.assertStrictTrue(evaluator.evaluate(0)) + self.assertStrictTrue(evaluator.evaluate(0)) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - lt_int_condition_list, {'meters_travelled': 47}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_int_condition_list, {'meters_travelled': 47}, self.mock_client_logger + ) - self.assertStrictTrue(evaluator.evaluate(0)) + self.assertStrictTrue(evaluator.evaluate(0)) - if PY2: - evaluator = condition_helper.CustomAttributeConditionEvaluator( - lt_int_condition_list, {'meters_travelled': long(47)}, self.mock_client_logger - ) + if PY2: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_int_condition_list, {'meters_travelled': long(47)}, self.mock_client_logger, + ) - self.assertStrictTrue(evaluator.evaluate(0)) + self.assertStrictTrue(evaluator.evaluate(0)) - def test_less_than_float__returns_true__when_user_value_less_than_condition_value(self): + def test_less_than_float__returns_true__when_user_value_less_than_condition_value(self,): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - lt_float_condition_list, {'meters_travelled': 48.1}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_float_condition_list, {'meters_travelled': 48.1}, self.mock_client_logger + ) - self.assertStrictTrue(evaluator.evaluate(0)) + self.assertStrictTrue(evaluator.evaluate(0)) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - lt_float_condition_list, {'meters_travelled': 48}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_float_condition_list, {'meters_travelled': 48}, self.mock_client_logger + ) - self.assertStrictTrue(evaluator.evaluate(0)) + self.assertStrictTrue(evaluator.evaluate(0)) - if PY2: - evaluator = condition_helper.CustomAttributeConditionEvaluator( - lt_float_condition_list, {'meters_travelled': long(48)}, self.mock_client_logger - ) + if PY2: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_float_condition_list, {'meters_travelled': long(48)}, self.mock_client_logger, + ) - self.assertStrictTrue(evaluator.evaluate(0)) + self.assertStrictTrue(evaluator.evaluate(0)) - def test_less_than_int__returns_false__when_user_value_not_less_than_condition_value(self): + def test_less_than_int__returns_false__when_user_value_not_less_than_condition_value(self,): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - lt_int_condition_list, {'meters_travelled': 48.1}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_int_condition_list, {'meters_travelled': 48.1}, self.mock_client_logger + ) - self.assertStrictFalse(evaluator.evaluate(0)) + self.assertStrictFalse(evaluator.evaluate(0)) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - lt_int_condition_list, {'meters_travelled': 49}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_int_condition_list, {'meters_travelled': 49}, self.mock_client_logger + ) - self.assertStrictFalse(evaluator.evaluate(0)) + self.assertStrictFalse(evaluator.evaluate(0)) - if PY2: - evaluator = condition_helper.CustomAttributeConditionEvaluator( - lt_int_condition_list, {'meters_travelled': long(49)}, self.mock_client_logger - ) + if PY2: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_int_condition_list, {'meters_travelled': long(49)}, self.mock_client_logger, + ) - self.assertStrictFalse(evaluator.evaluate(0)) + self.assertStrictFalse(evaluator.evaluate(0)) - def test_less_than_float__returns_false__when_user_value_not_less_than_condition_value(self): + def test_less_than_float__returns_false__when_user_value_not_less_than_condition_value(self,): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - lt_float_condition_list, {'meters_travelled': 48.2}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_float_condition_list, {'meters_travelled': 48.2}, self.mock_client_logger + ) - self.assertStrictFalse(evaluator.evaluate(0)) + self.assertStrictFalse(evaluator.evaluate(0)) - evaluator = condition_helper.CustomAttributeConditionEvaluator( - lt_float_condition_list, {'meters_travelled': 49}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_float_condition_list, {'meters_travelled': 49}, self.mock_client_logger + ) - self.assertStrictFalse(evaluator.evaluate(0)) + self.assertStrictFalse(evaluator.evaluate(0)) - if PY2: - evaluator = condition_helper.CustomAttributeConditionEvaluator( - lt_float_condition_list, {'meters_travelled': long(49)}, self.mock_client_logger - ) + if PY2: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_float_condition_list, {'meters_travelled': long(49)}, self.mock_client_logger, + ) - self.assertStrictFalse(evaluator.evaluate(0)) + self.assertStrictFalse(evaluator.evaluate(0)) - def test_less_than_int__returns_null__when_user_value_is_not_a_number(self): + def test_less_than_int__returns_null__when_user_value_is_not_a_number(self): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - lt_int_condition_list, {'meters_travelled': False}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_int_condition_list, {'meters_travelled': False}, self.mock_client_logger + ) - self.assertIsNone(evaluator.evaluate(0)) + self.assertIsNone(evaluator.evaluate(0)) - def test_less_than_float__returns_null__when_user_value_is_not_a_number(self): + def test_less_than_float__returns_null__when_user_value_is_not_a_number(self): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - lt_float_condition_list, {'meters_travelled': False}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_float_condition_list, {'meters_travelled': False}, self.mock_client_logger, + ) - self.assertIsNone(evaluator.evaluate(0)) + self.assertIsNone(evaluator.evaluate(0)) - def test_less_than_int__returns_null__when_no_user_provided_value(self): + def test_less_than_int__returns_null__when_no_user_provided_value(self): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - lt_int_condition_list, {}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_int_condition_list, {}, self.mock_client_logger + ) - self.assertIsNone(evaluator.evaluate(0)) + self.assertIsNone(evaluator.evaluate(0)) - def test_less_than_float__returns_null__when_no_user_provided_value(self): + def test_less_than_float__returns_null__when_no_user_provided_value(self): - evaluator = condition_helper.CustomAttributeConditionEvaluator( - lt_float_condition_list, {}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_float_condition_list, {}, self.mock_client_logger + ) - self.assertIsNone(evaluator.evaluate(0)) + self.assertIsNone(evaluator.evaluate(0)) - def test_greater_than__calls_is_finite_number(self): - """ Test that CustomAttributeConditionEvaluator.evaluate returns True + def test_greater_than__calls_is_finite_number(self): + """ Test that CustomAttributeConditionEvaluator.evaluate returns True if is_finite_number returns True. Returns None if is_finite_number returns False. """ - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_int_condition_list, {'meters_travelled': 48.1}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_int_condition_list, {'meters_travelled': 48.1}, self.mock_client_logger + ) - def is_finite_number__rejecting_condition_value(value): - if value == 48: - return False - return True + def is_finite_number__rejecting_condition_value(value): + if value == 48: + return False + return True - with mock.patch('optimizely.helpers.validator.is_finite_number', - side_effect=is_finite_number__rejecting_condition_value) as mock_is_finite: - self.assertIsNone(evaluator.evaluate(0)) + with mock.patch( + 'optimizely.helpers.validator.is_finite_number', side_effect=is_finite_number__rejecting_condition_value, + ) as mock_is_finite: + self.assertIsNone(evaluator.evaluate(0)) - # assert that isFiniteNumber only needs to reject condition value to stop evaluation. - mock_is_finite.assert_called_once_with(48) + # assert that isFiniteNumber only needs to reject condition value to stop evaluation. + mock_is_finite.assert_called_once_with(48) - def is_finite_number__rejecting_user_attribute_value(value): - if value == 48.1: - return False - return True + def is_finite_number__rejecting_user_attribute_value(value): + if value == 48.1: + return False + return True - with mock.patch('optimizely.helpers.validator.is_finite_number', - side_effect=is_finite_number__rejecting_user_attribute_value) as mock_is_finite: - self.assertIsNone(evaluator.evaluate(0)) + with mock.patch( + 'optimizely.helpers.validator.is_finite_number', + side_effect=is_finite_number__rejecting_user_attribute_value, + ) as mock_is_finite: + self.assertIsNone(evaluator.evaluate(0)) - # assert that isFiniteNumber evaluates user value only if it has accepted condition value. - mock_is_finite.assert_has_calls([mock.call(48), mock.call(48.1)]) + # assert that isFiniteNumber evaluates user value only if it has accepted condition value. + mock_is_finite.assert_has_calls([mock.call(48), mock.call(48.1)]) - def is_finite_number__accepting_both_values(value): - return True + def is_finite_number__accepting_both_values(value): + return True - with mock.patch('optimizely.helpers.validator.is_finite_number', - side_effect=is_finite_number__accepting_both_values): - self.assertTrue(evaluator.evaluate(0)) + with mock.patch( + 'optimizely.helpers.validator.is_finite_number', side_effect=is_finite_number__accepting_both_values, + ): + self.assertTrue(evaluator.evaluate(0)) - def test_less_than__calls_is_finite_number(self): - """ Test that CustomAttributeConditionEvaluator.evaluate returns True + def test_less_than__calls_is_finite_number(self): + """ Test that CustomAttributeConditionEvaluator.evaluate returns True if is_finite_number returns True. Returns None if is_finite_number returns False. """ - evaluator = condition_helper.CustomAttributeConditionEvaluator( - lt_int_condition_list, {'meters_travelled': 47}, self.mock_client_logger - ) + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_int_condition_list, {'meters_travelled': 47}, self.mock_client_logger + ) - def is_finite_number__rejecting_condition_value(value): - if value == 48: - return False - return True + def is_finite_number__rejecting_condition_value(value): + if value == 48: + return False + return True - with mock.patch('optimizely.helpers.validator.is_finite_number', - side_effect=is_finite_number__rejecting_condition_value) as mock_is_finite: - self.assertIsNone(evaluator.evaluate(0)) + with mock.patch( + 'optimizely.helpers.validator.is_finite_number', side_effect=is_finite_number__rejecting_condition_value, + ) as mock_is_finite: + self.assertIsNone(evaluator.evaluate(0)) - # assert that isFiniteNumber only needs to reject condition value to stop evaluation. - mock_is_finite.assert_called_once_with(48) + # assert that isFiniteNumber only needs to reject condition value to stop evaluation. + mock_is_finite.assert_called_once_with(48) - def is_finite_number__rejecting_user_attribute_value(value): - if value == 47: - return False - return True + def is_finite_number__rejecting_user_attribute_value(value): + if value == 47: + return False + return True - with mock.patch('optimizely.helpers.validator.is_finite_number', - side_effect=is_finite_number__rejecting_user_attribute_value) as mock_is_finite: - self.assertIsNone(evaluator.evaluate(0)) + with mock.patch( + 'optimizely.helpers.validator.is_finite_number', + side_effect=is_finite_number__rejecting_user_attribute_value, + ) as mock_is_finite: + self.assertIsNone(evaluator.evaluate(0)) - # assert that isFiniteNumber evaluates user value only if it has accepted condition value. - mock_is_finite.assert_has_calls([mock.call(48), mock.call(47)]) + # assert that isFiniteNumber evaluates user value only if it has accepted condition value. + mock_is_finite.assert_has_calls([mock.call(48), mock.call(47)]) - def is_finite_number__accepting_both_values(value): - return True + def is_finite_number__accepting_both_values(value): + return True - with mock.patch('optimizely.helpers.validator.is_finite_number', - side_effect=is_finite_number__accepting_both_values): - self.assertTrue(evaluator.evaluate(0)) + with mock.patch( + 'optimizely.helpers.validator.is_finite_number', side_effect=is_finite_number__accepting_both_values, + ): + self.assertTrue(evaluator.evaluate(0)) class ConditionDecoderTests(base.BaseTest): + def test_loads(self): + """ Test that loads correctly sets condition structure and list. """ - def test_loads(self): - """ Test that loads correctly sets condition structure and list. """ - - condition_structure, condition_list = condition_helper.loads( - self.config_dict['audiences'][0]['conditions'] - ) + condition_structure, condition_list = condition_helper.loads(self.config_dict['audiences'][0]['conditions']) - self.assertEqual(['and', ['or', ['or', 0]]], condition_structure) - self.assertEqual([['test_attribute', 'test_value_1', 'custom_attribute', None]], condition_list) + self.assertEqual(['and', ['or', ['or', 0]]], condition_structure) + self.assertEqual( + [['test_attribute', 'test_value_1', 'custom_attribute', None]], condition_list, + ) - def test_audience_condition_deserializer_defaults(self): - """ Test that audience_condition_deserializer defaults to None.""" + def test_audience_condition_deserializer_defaults(self): + """ Test that audience_condition_deserializer defaults to None.""" - browserConditionSafari = {} + browserConditionSafari = {} - items = condition_helper._audience_condition_deserializer(browserConditionSafari) - self.assertIsNone(items[0]) - self.assertIsNone(items[1]) - self.assertIsNone(items[2]) - self.assertIsNone(items[3]) + items = condition_helper._audience_condition_deserializer(browserConditionSafari) + self.assertIsNone(items[0]) + self.assertIsNone(items[1]) + self.assertIsNone(items[2]) + self.assertIsNone(items[3]) class CustomAttributeConditionEvaluatorLogging(base.BaseTest): - - def setUp(self): - base.BaseTest.setUp(self) - self.mock_client_logger = mock.MagicMock() - - def test_evaluate__match_type__invalid(self): - log_level = 'warning' - condition_list = [['favorite_constellation', 'Lacerta', 'custom_attribute', 'regex']] - user_attributes = {} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - condition_list, user_attributes, self.mock_client_logger - ) - - expected_condition_log = { - "name": 'favorite_constellation', - "value": 'Lacerta', - "type": 'custom_attribute', - "match": 'regex' - } - - self.assertIsNone(evaluator.evaluate(0)) - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition "{}" uses an unknown match ' - 'type. You may need to upgrade to a newer release of the Optimizely SDK.') - .format(json.dumps(expected_condition_log))) - - def test_evaluate__condition_type__invalid(self): - log_level = 'warning' - condition_list = [['favorite_constellation', 'Lacerta', 'sdk_version', 'exact']] - user_attributes = {} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - condition_list, user_attributes, self.mock_client_logger - ) - - expected_condition_log = { - "name": 'favorite_constellation', - "value": 'Lacerta', - "type": 'sdk_version', - "match": 'exact' - } - - self.assertIsNone(evaluator.evaluate(0)) - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition "{}" uses an unknown condition type. ' - 'You may need to upgrade to a newer release of the Optimizely SDK.').format(json.dumps(expected_condition_log))) - - def test_exact__user_value__missing(self): - log_level = 'debug' - exact_condition_list = [['favorite_constellation', 'Lacerta', 'custom_attribute', 'exact']] - user_attributes = {} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_condition_list, user_attributes, self.mock_client_logger - ) - - expected_condition_log = { - "name": 'favorite_constellation', - "value": 'Lacerta', - "type": 'custom_attribute', - "match": 'exact' - } - - self.assertIsNone(evaluator.evaluate(0)) - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition {} evaluated to UNKNOWN because ' - 'no value was passed for user attribute "favorite_constellation".').format(json.dumps(expected_condition_log))) - - def test_greater_than__user_value__missing(self): - log_level = 'debug' - gt_condition_list = [['meters_travelled', 48, 'custom_attribute', 'gt']] - user_attributes = {} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_condition_list, user_attributes, self.mock_client_logger - ) - - expected_condition_log = { - "name": 'meters_travelled', - "value": 48, - "type": 'custom_attribute', - "match": 'gt' - } - - self.assertIsNone(evaluator.evaluate(0)) - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition {} evaluated to UNKNOWN because no value was passed for user ' - 'attribute "meters_travelled".').format(json.dumps(expected_condition_log))) - - def test_less_than__user_value__missing(self): - log_level = 'debug' - lt_condition_list = [['meters_travelled', 48, 'custom_attribute', 'lt']] - user_attributes = {} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - lt_condition_list, user_attributes, self.mock_client_logger - ) - - expected_condition_log = { - "name": 'meters_travelled', - "value": 48, - "type": 'custom_attribute', - "match": 'lt' - } - - self.assertIsNone(evaluator.evaluate(0)) - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition {} evaluated to UNKNOWN because no value was passed for user attribute ' - '"meters_travelled".').format(json.dumps(expected_condition_log))) - - def test_substring__user_value__missing(self): - log_level = 'debug' - substring_condition_list = [['headline_text', 'buy now', 'custom_attribute', 'substring']] - user_attributes = {} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - substring_condition_list, user_attributes, self.mock_client_logger - ) - - expected_condition_log = { - "name": 'headline_text', - "value": 'buy now', - "type": 'custom_attribute', - "match": 'substring' - } - - self.assertIsNone(evaluator.evaluate(0)) - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition {} evaluated to UNKNOWN because no value was passed for ' - 'user attribute "headline_text".').format(json.dumps(expected_condition_log))) - - def test_exists__user_value__missing(self): - exists_condition_list = [['input_value', None, 'custom_attribute', 'exists']] - user_attributes = {} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exists_condition_list, user_attributes, self.mock_client_logger - ) - - self.assertStrictFalse(evaluator.evaluate(0)) - - self.mock_client_logger.debug.assert_not_called() - self.mock_client_logger.info.assert_not_called() - self.mock_client_logger.warning.assert_not_called() - - def test_exact__user_value__None(self): - log_level = 'debug' - exact_condition_list = [['favorite_constellation', 'Lacerta', 'custom_attribute', 'exact']] - user_attributes = {'favorite_constellation': None} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_condition_list, user_attributes, self.mock_client_logger - ) - - expected_condition_log = { - "name": 'favorite_constellation', - "value": 'Lacerta', - "type": 'custom_attribute', - "match": 'exact' - } - - self.assertIsNone(evaluator.evaluate(0)) - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition "{}" evaluated to UNKNOWN because a null value was passed for user attribute ' - '"favorite_constellation".').format(json.dumps(expected_condition_log))) - - def test_greater_than__user_value__None(self): - log_level = 'debug' - gt_condition_list = [['meters_travelled', 48, 'custom_attribute', 'gt']] - user_attributes = {'meters_travelled': None} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_condition_list, user_attributes, self.mock_client_logger - ) - - expected_condition_log = { - "name": 'meters_travelled', - "value": 48, - "type": 'custom_attribute', - "match": 'gt' - } - - self.assertIsNone(evaluator.evaluate(0)) - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition "{}" evaluated to UNKNOWN because a null value was passed for ' - 'user attribute "meters_travelled".').format(json.dumps(expected_condition_log))) - - def test_less_than__user_value__None(self): - log_level = 'debug' - lt_condition_list = [['meters_travelled', 48, 'custom_attribute', 'lt']] - user_attributes = {'meters_travelled': None} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - lt_condition_list, user_attributes, self.mock_client_logger - ) - - expected_condition_log = { - "name": 'meters_travelled', - "value": 48, - "type": 'custom_attribute', - "match": 'lt' - } - - self.assertIsNone(evaluator.evaluate(0)) - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition "{}" evaluated to UNKNOWN because a null value was passed ' - 'for user attribute "meters_travelled".').format(json.dumps(expected_condition_log))) - - def test_substring__user_value__None(self): - log_level = 'debug' - substring_condition_list = [['headline_text', '12', 'custom_attribute', 'substring']] - user_attributes = {'headline_text': None} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - substring_condition_list, user_attributes, self.mock_client_logger - ) - - expected_condition_log = { - "name": 'headline_text', - "value": '12', - "type": 'custom_attribute', - "match": 'substring' - } - - self.assertIsNone(evaluator.evaluate(0)) - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition "{}" evaluated to UNKNOWN because a null value was ' - 'passed for user attribute "headline_text".').format(json.dumps(expected_condition_log))) - - def test_exists__user_value__None(self): - exists_condition_list = [['input_value', None, 'custom_attribute', 'exists']] - user_attributes = {'input_value': None} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exists_condition_list, user_attributes, self.mock_client_logger - ) - - self.assertStrictFalse(evaluator.evaluate(0)) - - self.mock_client_logger.debug.assert_not_called() - self.mock_client_logger.info.assert_not_called() - self.mock_client_logger.warning.assert_not_called() - - def test_exact__user_value__unexpected_type(self): - log_level = 'warning' - exact_condition_list = [['favorite_constellation', 'Lacerta', 'custom_attribute', 'exact']] - user_attributes = {'favorite_constellation': {}} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_condition_list, user_attributes, self.mock_client_logger - ) - - expected_condition_log = { - "name": 'favorite_constellation', - "value": 'Lacerta', - "type": 'custom_attribute', - "match": 'exact' - } - - self.assertIsNone(evaluator.evaluate(0)) - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition "{}" evaluated to UNKNOWN because a value of type "{}" was passed for ' - 'user attribute "favorite_constellation".').format(json.dumps(expected_condition_log), type({}))) - - def test_greater_than__user_value__unexpected_type(self): - log_level = 'warning' - gt_condition_list = [['meters_travelled', 48, 'custom_attribute', 'gt']] - user_attributes = {'meters_travelled': '48'} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_condition_list, user_attributes, self.mock_client_logger - ) - - expected_condition_log = { - "name": 'meters_travelled', - "value": 48, - "type": 'custom_attribute', - "match": 'gt' - } - - self.assertIsNone(evaluator.evaluate(0)) - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition "{}"' - ' evaluated to UNKNOWN because a value of type "{}" was passed for user attribute ' - '"meters_travelled".').format(json.dumps(expected_condition_log), type('48'))) - - def test_less_than__user_value__unexpected_type(self): - log_level = 'warning' - lt_condition_list = [['meters_travelled', 48, 'custom_attribute', 'lt']] - user_attributes = {'meters_travelled': True} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - lt_condition_list, user_attributes, self.mock_client_logger - ) - - expected_condition_log = { - "name": 'meters_travelled', - "value": 48, - "type": 'custom_attribute', - "match": 'lt' - } - - self.assertIsNone(evaluator.evaluate(0)) - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition "{}"' - ' evaluated to UNKNOWN because a value of type "{}" was passed for user attribute ' - '"meters_travelled".').format(json.dumps(expected_condition_log), type(True))) - - def test_substring__user_value__unexpected_type(self): - log_level = 'warning' - substring_condition_list = [['headline_text', '12', 'custom_attribute', 'substring']] - user_attributes = {'headline_text': 1234} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - substring_condition_list, user_attributes, self.mock_client_logger - ) - - expected_condition_log = { - "name": 'headline_text', - "value": '12', - "type": 'custom_attribute', - "match": 'substring' - } - - self.assertIsNone(evaluator.evaluate(0)) - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition "{}" evaluated to UNKNOWN because a value of type "{}" was passed for ' - 'user attribute "headline_text".').format(json.dumps(expected_condition_log), type(1234))) - - def test_exact__user_value__infinite(self): - log_level = 'warning' - exact_condition_list = [['meters_travelled', 48, 'custom_attribute', 'exact']] - user_attributes = {'meters_travelled': float("inf")} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_condition_list, user_attributes, self.mock_client_logger - ) - - self.assertIsNone(evaluator.evaluate(0)) - - expected_condition_log = { - "name": 'meters_travelled', - "value": 48, - "type": 'custom_attribute', - "match": 'exact' - } - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition "{}" evaluated to UNKNOWN because the number value for ' - 'user attribute "meters_travelled" is not in the range [-2^53, +2^53].' - ).format(json.dumps(expected_condition_log))) - - def test_greater_than__user_value__infinite(self): - log_level = 'warning' - gt_condition_list = [['meters_travelled', 48, 'custom_attribute', 'gt']] - user_attributes = {'meters_travelled': float("nan")} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_condition_list, user_attributes, self.mock_client_logger - ) - - expected_condition_log = { - "name": 'meters_travelled', - "value": 48, - "type": 'custom_attribute', - "match": 'gt' - } - - self.assertIsNone(evaluator.evaluate(0)) - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition "{}" ' - 'evaluated to UNKNOWN because the number value for user attribute "meters_travelled" is not' - ' in the range [-2^53, +2^53].').format(json.dumps(expected_condition_log))) - - def test_less_than__user_value__infinite(self): - log_level = 'warning' - lt_condition_list = [['meters_travelled', 48, 'custom_attribute', 'lt']] - user_attributes = {'meters_travelled': float('-inf')} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - lt_condition_list, user_attributes, self.mock_client_logger - ) - - expected_condition_log = { - "name": 'meters_travelled', - "value": 48, - "type": 'custom_attribute', - "match": 'lt' - } - - self.assertIsNone(evaluator.evaluate(0)) - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition "{}" ' - 'evaluated to UNKNOWN because the number value for user attribute "meters_travelled" is not in ' - 'the range [-2^53, +2^53].').format(json.dumps(expected_condition_log))) - - def test_exact__user_value_type_mismatch(self): - log_level = 'warning' - exact_condition_list = [['favorite_constellation', 'Lacerta', 'custom_attribute', 'exact']] - user_attributes = {'favorite_constellation': 5} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_condition_list, user_attributes, self.mock_client_logger - ) - - expected_condition_log = { - "name": 'favorite_constellation', - "value": 'Lacerta', - "type": 'custom_attribute', - "match": 'exact' - } - - self.assertIsNone(evaluator.evaluate(0)) - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition "{}" evaluated to UNKNOWN because a value of type "{}" was passed for ' - 'user attribute "favorite_constellation".').format(json.dumps(expected_condition_log), type(5))) - - def test_exact__condition_value_invalid(self): - log_level = 'warning' - exact_condition_list = [['favorite_constellation', {}, 'custom_attribute', 'exact']] - user_attributes = {'favorite_constellation': 'Lacerta'} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_condition_list, user_attributes, self.mock_client_logger - ) - - expected_condition_log = { - "name": 'favorite_constellation', - "value": {}, - "type": 'custom_attribute', - "match": 'exact' - } - - self.assertIsNone(evaluator.evaluate(0)) - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition "{}" has an unsupported condition value. You may need to upgrade to a ' - 'newer release of the Optimizely SDK.').format(json.dumps(expected_condition_log))) - - def test_exact__condition_value_infinite(self): - log_level = 'warning' - exact_condition_list = [['favorite_constellation', float('inf'), 'custom_attribute', 'exact']] - user_attributes = {'favorite_constellation': 'Lacerta'} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - exact_condition_list, user_attributes, self.mock_client_logger - ) - - expected_condition_log = { - "name": 'favorite_constellation', - "value": float('inf'), - "type": 'custom_attribute', - "match": 'exact' - } - - self.assertIsNone(evaluator.evaluate(0)) - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition "{}" has an unsupported condition value. You may need to upgrade to a ' - 'newer release of the Optimizely SDK.').format(json.dumps(expected_condition_log))) - - def test_greater_than__condition_value_invalid(self): - log_level = 'warning' - gt_condition_list = [['meters_travelled', True, 'custom_attribute', 'gt']] - user_attributes = {'meters_travelled': 48} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_condition_list, user_attributes, self.mock_client_logger - ) - - expected_condition_log = { - "name": 'meters_travelled', - "value": True, - "type": 'custom_attribute', - "match": 'gt' - } - - self.assertIsNone(evaluator.evaluate(0)) - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition "{}" has an unsupported condition value. You may need to upgrade to a ' - 'newer release of the Optimizely SDK.').format(json.dumps(expected_condition_log))) - - def test_less_than__condition_value_invalid(self): - log_level = 'warning' - gt_condition_list = [['meters_travelled', float('nan'), 'custom_attribute', 'lt']] - user_attributes = {'meters_travelled': 48} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - gt_condition_list, user_attributes, self.mock_client_logger - ) - - expected_condition_log = { - "name": 'meters_travelled', - "value": float('nan'), - "type": 'custom_attribute', - "match": 'lt' - } - - self.assertIsNone(evaluator.evaluate(0)) - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition "{}" has an unsupported condition value. You may need to upgrade to a ' - 'newer release of the Optimizely SDK.').format(json.dumps(expected_condition_log))) - - def test_substring__condition_value_invalid(self): - log_level = 'warning' - substring_condition_list = [['headline_text', False, 'custom_attribute', 'substring']] - user_attributes = {'headline_text': 'breaking news'} - - evaluator = condition_helper.CustomAttributeConditionEvaluator( - substring_condition_list, user_attributes, self.mock_client_logger - ) - - expected_condition_log = { - "name": 'headline_text', - "value": False, - "type": 'custom_attribute', - "match": 'substring' - } - - self.assertIsNone(evaluator.evaluate(0)) - - mock_log = getattr(self.mock_client_logger, log_level) - mock_log.assert_called_once_with(( - 'Audience condition "{}" has an unsupported condition value. You may need to upgrade to a ' - 'newer release of the Optimizely SDK.').format(json.dumps(expected_condition_log))) + def setUp(self): + base.BaseTest.setUp(self) + self.mock_client_logger = mock.MagicMock() + + def test_evaluate__match_type__invalid(self): + log_level = 'warning' + condition_list = [['favorite_constellation', 'Lacerta', 'custom_attribute', 'regex']] + user_attributes = {} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + condition_list, user_attributes, self.mock_client_logger + ) + + expected_condition_log = { + "name": 'favorite_constellation', + "value": 'Lacerta', + "type": 'custom_attribute', + "match": 'regex', + } + + self.assertIsNone(evaluator.evaluate(0)) + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition "{}" uses an unknown match ' + 'type. You may need to upgrade to a newer release of the Optimizely SDK.' + ).format(json.dumps(expected_condition_log)) + ) + + def test_evaluate__condition_type__invalid(self): + log_level = 'warning' + condition_list = [['favorite_constellation', 'Lacerta', 'sdk_version', 'exact']] + user_attributes = {} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + condition_list, user_attributes, self.mock_client_logger + ) + + expected_condition_log = { + "name": 'favorite_constellation', + "value": 'Lacerta', + "type": 'sdk_version', + "match": 'exact', + } + + self.assertIsNone(evaluator.evaluate(0)) + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition "{}" uses an unknown condition type. ' + 'You may need to upgrade to a newer release of the Optimizely SDK.' + ).format(json.dumps(expected_condition_log)) + ) + + def test_exact__user_value__missing(self): + log_level = 'debug' + exact_condition_list = [['favorite_constellation', 'Lacerta', 'custom_attribute', 'exact']] + user_attributes = {} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_condition_list, user_attributes, self.mock_client_logger + ) + + expected_condition_log = { + "name": 'favorite_constellation', + "value": 'Lacerta', + "type": 'custom_attribute', + "match": 'exact', + } + + self.assertIsNone(evaluator.evaluate(0)) + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition {} evaluated to UNKNOWN because ' + 'no value was passed for user attribute "favorite_constellation".' + ).format(json.dumps(expected_condition_log)) + ) + + def test_greater_than__user_value__missing(self): + log_level = 'debug' + gt_condition_list = [['meters_travelled', 48, 'custom_attribute', 'gt']] + user_attributes = {} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_condition_list, user_attributes, self.mock_client_logger + ) + + expected_condition_log = { + "name": 'meters_travelled', + "value": 48, + "type": 'custom_attribute', + "match": 'gt', + } + + self.assertIsNone(evaluator.evaluate(0)) + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition {} evaluated to UNKNOWN because no value was passed for user ' + 'attribute "meters_travelled".' + ).format(json.dumps(expected_condition_log)) + ) + + def test_less_than__user_value__missing(self): + log_level = 'debug' + lt_condition_list = [['meters_travelled', 48, 'custom_attribute', 'lt']] + user_attributes = {} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_condition_list, user_attributes, self.mock_client_logger + ) + + expected_condition_log = { + "name": 'meters_travelled', + "value": 48, + "type": 'custom_attribute', + "match": 'lt', + } + + self.assertIsNone(evaluator.evaluate(0)) + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition {} evaluated to UNKNOWN because no value was passed for user attribute ' + '"meters_travelled".' + ).format(json.dumps(expected_condition_log)) + ) + + def test_substring__user_value__missing(self): + log_level = 'debug' + substring_condition_list = [['headline_text', 'buy now', 'custom_attribute', 'substring']] + user_attributes = {} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + substring_condition_list, user_attributes, self.mock_client_logger + ) + + expected_condition_log = { + "name": 'headline_text', + "value": 'buy now', + "type": 'custom_attribute', + "match": 'substring', + } + + self.assertIsNone(evaluator.evaluate(0)) + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition {} evaluated to UNKNOWN because no value was passed for ' + 'user attribute "headline_text".' + ).format(json.dumps(expected_condition_log)) + ) + + def test_exists__user_value__missing(self): + exists_condition_list = [['input_value', None, 'custom_attribute', 'exists']] + user_attributes = {} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exists_condition_list, user_attributes, self.mock_client_logger + ) + + self.assertStrictFalse(evaluator.evaluate(0)) + + self.mock_client_logger.debug.assert_not_called() + self.mock_client_logger.info.assert_not_called() + self.mock_client_logger.warning.assert_not_called() + + def test_exact__user_value__None(self): + log_level = 'debug' + exact_condition_list = [['favorite_constellation', 'Lacerta', 'custom_attribute', 'exact']] + user_attributes = {'favorite_constellation': None} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_condition_list, user_attributes, self.mock_client_logger + ) + + expected_condition_log = { + "name": 'favorite_constellation', + "value": 'Lacerta', + "type": 'custom_attribute', + "match": 'exact', + } + + self.assertIsNone(evaluator.evaluate(0)) + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition "{}" evaluated to UNKNOWN because a null value was passed for user attribute ' + '"favorite_constellation".' + ).format(json.dumps(expected_condition_log)) + ) + + def test_greater_than__user_value__None(self): + log_level = 'debug' + gt_condition_list = [['meters_travelled', 48, 'custom_attribute', 'gt']] + user_attributes = {'meters_travelled': None} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_condition_list, user_attributes, self.mock_client_logger + ) + + expected_condition_log = { + "name": 'meters_travelled', + "value": 48, + "type": 'custom_attribute', + "match": 'gt', + } + + self.assertIsNone(evaluator.evaluate(0)) + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition "{}" evaluated to UNKNOWN because a null value was passed for ' + 'user attribute "meters_travelled".' + ).format(json.dumps(expected_condition_log)) + ) + + def test_less_than__user_value__None(self): + log_level = 'debug' + lt_condition_list = [['meters_travelled', 48, 'custom_attribute', 'lt']] + user_attributes = {'meters_travelled': None} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_condition_list, user_attributes, self.mock_client_logger + ) + + expected_condition_log = { + "name": 'meters_travelled', + "value": 48, + "type": 'custom_attribute', + "match": 'lt', + } + + self.assertIsNone(evaluator.evaluate(0)) + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition "{}" evaluated to UNKNOWN because a null value was passed ' + 'for user attribute "meters_travelled".' + ).format(json.dumps(expected_condition_log)) + ) + + def test_substring__user_value__None(self): + log_level = 'debug' + substring_condition_list = [['headline_text', '12', 'custom_attribute', 'substring']] + user_attributes = {'headline_text': None} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + substring_condition_list, user_attributes, self.mock_client_logger + ) + + expected_condition_log = { + "name": 'headline_text', + "value": '12', + "type": 'custom_attribute', + "match": 'substring', + } + + self.assertIsNone(evaluator.evaluate(0)) + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition "{}" evaluated to UNKNOWN because a null value was ' + 'passed for user attribute "headline_text".' + ).format(json.dumps(expected_condition_log)) + ) + + def test_exists__user_value__None(self): + exists_condition_list = [['input_value', None, 'custom_attribute', 'exists']] + user_attributes = {'input_value': None} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exists_condition_list, user_attributes, self.mock_client_logger + ) + + self.assertStrictFalse(evaluator.evaluate(0)) + + self.mock_client_logger.debug.assert_not_called() + self.mock_client_logger.info.assert_not_called() + self.mock_client_logger.warning.assert_not_called() + + def test_exact__user_value__unexpected_type(self): + log_level = 'warning' + exact_condition_list = [['favorite_constellation', 'Lacerta', 'custom_attribute', 'exact']] + user_attributes = {'favorite_constellation': {}} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_condition_list, user_attributes, self.mock_client_logger + ) + + expected_condition_log = { + "name": 'favorite_constellation', + "value": 'Lacerta', + "type": 'custom_attribute', + "match": 'exact', + } + + self.assertIsNone(evaluator.evaluate(0)) + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition "{}" evaluated to UNKNOWN because a value of type "{}" was passed for ' + 'user attribute "favorite_constellation".' + ).format(json.dumps(expected_condition_log), type({})) + ) + + def test_greater_than__user_value__unexpected_type(self): + log_level = 'warning' + gt_condition_list = [['meters_travelled', 48, 'custom_attribute', 'gt']] + user_attributes = {'meters_travelled': '48'} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_condition_list, user_attributes, self.mock_client_logger + ) + + expected_condition_log = { + "name": 'meters_travelled', + "value": 48, + "type": 'custom_attribute', + "match": 'gt', + } + + self.assertIsNone(evaluator.evaluate(0)) + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition "{}"' + ' evaluated to UNKNOWN because a value of type "{}" was passed for user attribute ' + '"meters_travelled".' + ).format(json.dumps(expected_condition_log), type('48')) + ) + + def test_less_than__user_value__unexpected_type(self): + log_level = 'warning' + lt_condition_list = [['meters_travelled', 48, 'custom_attribute', 'lt']] + user_attributes = {'meters_travelled': True} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_condition_list, user_attributes, self.mock_client_logger + ) + + expected_condition_log = { + "name": 'meters_travelled', + "value": 48, + "type": 'custom_attribute', + "match": 'lt', + } + + self.assertIsNone(evaluator.evaluate(0)) + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition "{}"' + ' evaluated to UNKNOWN because a value of type "{}" was passed for user attribute ' + '"meters_travelled".' + ).format(json.dumps(expected_condition_log), type(True)) + ) + + def test_substring__user_value__unexpected_type(self): + log_level = 'warning' + substring_condition_list = [['headline_text', '12', 'custom_attribute', 'substring']] + user_attributes = {'headline_text': 1234} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + substring_condition_list, user_attributes, self.mock_client_logger + ) + + expected_condition_log = { + "name": 'headline_text', + "value": '12', + "type": 'custom_attribute', + "match": 'substring', + } + + self.assertIsNone(evaluator.evaluate(0)) + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition "{}" evaluated to UNKNOWN because a value of type "{}" was passed for ' + 'user attribute "headline_text".' + ).format(json.dumps(expected_condition_log), type(1234)) + ) + + def test_exact__user_value__infinite(self): + log_level = 'warning' + exact_condition_list = [['meters_travelled', 48, 'custom_attribute', 'exact']] + user_attributes = {'meters_travelled': float("inf")} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_condition_list, user_attributes, self.mock_client_logger + ) + + self.assertIsNone(evaluator.evaluate(0)) + + expected_condition_log = { + "name": 'meters_travelled', + "value": 48, + "type": 'custom_attribute', + "match": 'exact', + } + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition "{}" evaluated to UNKNOWN because the number value for ' + 'user attribute "meters_travelled" is not in the range [-2^53, +2^53].' + ).format(json.dumps(expected_condition_log)) + ) + + def test_greater_than__user_value__infinite(self): + log_level = 'warning' + gt_condition_list = [['meters_travelled', 48, 'custom_attribute', 'gt']] + user_attributes = {'meters_travelled': float("nan")} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_condition_list, user_attributes, self.mock_client_logger + ) + + expected_condition_log = { + "name": 'meters_travelled', + "value": 48, + "type": 'custom_attribute', + "match": 'gt', + } + + self.assertIsNone(evaluator.evaluate(0)) + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition "{}" ' + 'evaluated to UNKNOWN because the number value for user attribute "meters_travelled" is not' + ' in the range [-2^53, +2^53].' + ).format(json.dumps(expected_condition_log)) + ) + + def test_less_than__user_value__infinite(self): + log_level = 'warning' + lt_condition_list = [['meters_travelled', 48, 'custom_attribute', 'lt']] + user_attributes = {'meters_travelled': float('-inf')} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_condition_list, user_attributes, self.mock_client_logger + ) + + expected_condition_log = { + "name": 'meters_travelled', + "value": 48, + "type": 'custom_attribute', + "match": 'lt', + } + + self.assertIsNone(evaluator.evaluate(0)) + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition "{}" ' + 'evaluated to UNKNOWN because the number value for user attribute "meters_travelled" is not in ' + 'the range [-2^53, +2^53].' + ).format(json.dumps(expected_condition_log)) + ) + + def test_exact__user_value_type_mismatch(self): + log_level = 'warning' + exact_condition_list = [['favorite_constellation', 'Lacerta', 'custom_attribute', 'exact']] + user_attributes = {'favorite_constellation': 5} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_condition_list, user_attributes, self.mock_client_logger + ) + + expected_condition_log = { + "name": 'favorite_constellation', + "value": 'Lacerta', + "type": 'custom_attribute', + "match": 'exact', + } + + self.assertIsNone(evaluator.evaluate(0)) + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition "{}" evaluated to UNKNOWN because a value of type "{}" was passed for ' + 'user attribute "favorite_constellation".' + ).format(json.dumps(expected_condition_log), type(5)) + ) + + def test_exact__condition_value_invalid(self): + log_level = 'warning' + exact_condition_list = [['favorite_constellation', {}, 'custom_attribute', 'exact']] + user_attributes = {'favorite_constellation': 'Lacerta'} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_condition_list, user_attributes, self.mock_client_logger + ) + + expected_condition_log = { + "name": 'favorite_constellation', + "value": {}, + "type": 'custom_attribute', + "match": 'exact', + } + + self.assertIsNone(evaluator.evaluate(0)) + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition "{}" has an unsupported condition value. You may need to upgrade to a ' + 'newer release of the Optimizely SDK.' + ).format(json.dumps(expected_condition_log)) + ) + + def test_exact__condition_value_infinite(self): + log_level = 'warning' + exact_condition_list = [['favorite_constellation', float('inf'), 'custom_attribute', 'exact']] + user_attributes = {'favorite_constellation': 'Lacerta'} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + exact_condition_list, user_attributes, self.mock_client_logger + ) + + expected_condition_log = { + "name": 'favorite_constellation', + "value": float('inf'), + "type": 'custom_attribute', + "match": 'exact', + } + + self.assertIsNone(evaluator.evaluate(0)) + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition "{}" has an unsupported condition value. You may need to upgrade to a ' + 'newer release of the Optimizely SDK.' + ).format(json.dumps(expected_condition_log)) + ) + + def test_greater_than__condition_value_invalid(self): + log_level = 'warning' + gt_condition_list = [['meters_travelled', True, 'custom_attribute', 'gt']] + user_attributes = {'meters_travelled': 48} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_condition_list, user_attributes, self.mock_client_logger + ) + + expected_condition_log = { + "name": 'meters_travelled', + "value": True, + "type": 'custom_attribute', + "match": 'gt', + } + + self.assertIsNone(evaluator.evaluate(0)) + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition "{}" has an unsupported condition value. You may need to upgrade to a ' + 'newer release of the Optimizely SDK.' + ).format(json.dumps(expected_condition_log)) + ) + + def test_less_than__condition_value_invalid(self): + log_level = 'warning' + gt_condition_list = [['meters_travelled', float('nan'), 'custom_attribute', 'lt']] + user_attributes = {'meters_travelled': 48} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_condition_list, user_attributes, self.mock_client_logger + ) + + expected_condition_log = { + "name": 'meters_travelled', + "value": float('nan'), + "type": 'custom_attribute', + "match": 'lt', + } + + self.assertIsNone(evaluator.evaluate(0)) + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition "{}" has an unsupported condition value. You may need to upgrade to a ' + 'newer release of the Optimizely SDK.' + ).format(json.dumps(expected_condition_log)) + ) + + def test_substring__condition_value_invalid(self): + log_level = 'warning' + substring_condition_list = [['headline_text', False, 'custom_attribute', 'substring']] + user_attributes = {'headline_text': 'breaking news'} + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + substring_condition_list, user_attributes, self.mock_client_logger + ) + + expected_condition_log = { + "name": 'headline_text', + "value": False, + "type": 'custom_attribute', + "match": 'substring', + } + + self.assertIsNone(evaluator.evaluate(0)) + + mock_log = getattr(self.mock_client_logger, log_level) + mock_log.assert_called_once_with( + ( + 'Audience condition "{}" has an unsupported condition value. You may need to upgrade to a ' + 'newer release of the Optimizely SDK.' + ).format(json.dumps(expected_condition_log)) + ) diff --git a/tests/helpers_tests/test_condition_tree_evaluator.py b/tests/helpers_tests/test_condition_tree_evaluator.py index 54aa7e92..63405b90 100644 --- a/tests/helpers_tests/test_condition_tree_evaluator.py +++ b/tests/helpers_tests/test_condition_tree_evaluator.py @@ -37,224 +37,148 @@ class ConditionTreeEvaluatorTests(base.BaseTest): + def test_evaluate__returns_true(self): + """ Test that evaluate returns True when the leaf condition evaluator returns True. """ - def test_evaluate__returns_true(self): - """ Test that evaluate returns True when the leaf condition evaluator returns True. """ + self.assertStrictTrue(evaluate(conditionA, lambda a: True)) - self.assertStrictTrue(evaluate(conditionA, lambda a: True)) + def test_evaluate__returns_false(self): + """ Test that evaluate returns False when the leaf condition evaluator returns False. """ - def test_evaluate__returns_false(self): - """ Test that evaluate returns False when the leaf condition evaluator returns False. """ + self.assertStrictFalse(evaluate(conditionA, lambda a: False)) - self.assertStrictFalse(evaluate(conditionA, lambda a: False)) + def test_and_evaluator__returns_true(self): + """ Test that and_evaluator returns True when all conditions evaluate to True. """ - def test_and_evaluator__returns_true(self): - """ Test that and_evaluator returns True when all conditions evaluate to True. """ + self.assertStrictTrue(evaluate(['and', conditionA, conditionB], lambda a: True)) - self.assertStrictTrue(evaluate( - ['and', conditionA, conditionB], - lambda a: True - )) + def test_and_evaluator__returns_false(self): + """ Test that and_evaluator returns False when any one condition evaluates to False. """ - def test_and_evaluator__returns_false(self): - """ Test that and_evaluator returns False when any one condition evaluates to False. """ + leafEvaluator = mock.MagicMock(side_effect=[True, False]) - leafEvaluator = mock.MagicMock(side_effect=[True, False]) + self.assertStrictFalse(evaluate(['and', conditionA, conditionB], lambda a: leafEvaluator())) - self.assertStrictFalse(evaluate( - ['and', conditionA, conditionB], - lambda a: leafEvaluator() - )) + def test_and_evaluator__returns_null__when_all_null(self): + """ Test that and_evaluator returns null when all operands evaluate to null. """ - def test_and_evaluator__returns_null__when_all_null(self): - """ Test that and_evaluator returns null when all operands evaluate to null. """ + self.assertIsNone(evaluate(['and', conditionA, conditionB], lambda a: None)) - self.assertIsNone(evaluate( - ['and', conditionA, conditionB], - lambda a: None - )) + def test_and_evaluator__returns_null__when_trues_and_null(self): + """ Test that and_evaluator returns when operands evaluate to trues and null. """ - def test_and_evaluator__returns_null__when_trues_and_null(self): - """ Test that and_evaluator returns when operands evaluate to trues and null. """ + leafEvaluator = mock.MagicMock(side_effect=[True, None]) - leafEvaluator = mock.MagicMock(side_effect=[True, None]) + self.assertIsNone(evaluate(['and', conditionA, conditionB], lambda a: leafEvaluator())) - self.assertIsNone(evaluate( - ['and', conditionA, conditionB], - lambda a: leafEvaluator() - )) + leafEvaluator = mock.MagicMock(side_effect=[None, True]) - leafEvaluator = mock.MagicMock(side_effect=[None, True]) + self.assertIsNone(evaluate(['and', conditionA, conditionB], lambda a: leafEvaluator())) - self.assertIsNone(evaluate( - ['and', conditionA, conditionB], - lambda a: leafEvaluator() - )) + def test_and_evaluator__returns_false__when_falses_and_null(self): + """ Test that and_evaluator returns False when when operands evaluate to falses and null. """ - def test_and_evaluator__returns_false__when_falses_and_null(self): - """ Test that and_evaluator returns False when when operands evaluate to falses and null. """ + leafEvaluator = mock.MagicMock(side_effect=[False, None]) - leafEvaluator = mock.MagicMock(side_effect=[False, None]) + self.assertStrictFalse(evaluate(['and', conditionA, conditionB], lambda a: leafEvaluator())) - self.assertStrictFalse(evaluate( - ['and', conditionA, conditionB], - lambda a: leafEvaluator() - )) + leafEvaluator = mock.MagicMock(side_effect=[None, False]) - leafEvaluator = mock.MagicMock(side_effect=[None, False]) + self.assertStrictFalse(evaluate(['and', conditionA, conditionB], lambda a: leafEvaluator())) - self.assertStrictFalse(evaluate( - ['and', conditionA, conditionB], - lambda a: leafEvaluator() - )) + def test_and_evaluator__returns_false__when_trues_falses_and_null(self): + """ Test that and_evaluator returns False when operands evaluate to trues, falses and null. """ - def test_and_evaluator__returns_false__when_trues_falses_and_null(self): - """ Test that and_evaluator returns False when operands evaluate to trues, falses and null. """ + leafEvaluator = mock.MagicMock(side_effect=[True, False, None]) - leafEvaluator = mock.MagicMock(side_effect=[True, False, None]) + self.assertStrictFalse(evaluate(['and', conditionA, conditionB], lambda a: leafEvaluator())) - self.assertStrictFalse(evaluate( - ['and', conditionA, conditionB], - lambda a: leafEvaluator() - )) + def test_or_evaluator__returns_true__when_any_true(self): + """ Test that or_evaluator returns True when any one condition evaluates to True. """ - def test_or_evaluator__returns_true__when_any_true(self): - """ Test that or_evaluator returns True when any one condition evaluates to True. """ + leafEvaluator = mock.MagicMock(side_effect=[False, True]) - leafEvaluator = mock.MagicMock(side_effect=[False, True]) + self.assertStrictTrue(evaluate(['or', conditionA, conditionB], lambda a: leafEvaluator())) - self.assertStrictTrue(evaluate( - ['or', conditionA, conditionB], - lambda a: leafEvaluator() - )) + def test_or_evaluator__returns_false__when_all_false(self): + """ Test that or_evaluator returns False when all operands evaluate to False.""" - def test_or_evaluator__returns_false__when_all_false(self): - """ Test that or_evaluator returns False when all operands evaluate to False.""" + self.assertStrictFalse(evaluate(['or', conditionA, conditionB], lambda a: False)) - self.assertStrictFalse(evaluate( - ['or', conditionA, conditionB], - lambda a: False - )) + def test_or_evaluator__returns_null__when_all_null(self): + """ Test that or_evaluator returns null when all operands evaluate to null. """ - def test_or_evaluator__returns_null__when_all_null(self): - """ Test that or_evaluator returns null when all operands evaluate to null. """ + self.assertIsNone(evaluate(['or', conditionA, conditionB], lambda a: None)) - self.assertIsNone(evaluate( - ['or', conditionA, conditionB], - lambda a: None - )) + def test_or_evaluator__returns_true__when_trues_and_null(self): + """ Test that or_evaluator returns True when operands evaluate to trues and null. """ - def test_or_evaluator__returns_true__when_trues_and_null(self): - """ Test that or_evaluator returns True when operands evaluate to trues and null. """ + leafEvaluator = mock.MagicMock(side_effect=[None, True]) - leafEvaluator = mock.MagicMock(side_effect=[None, True]) + self.assertStrictTrue(evaluate(['or', conditionA, conditionB], lambda a: leafEvaluator())) - self.assertStrictTrue(evaluate( - ['or', conditionA, conditionB], - lambda a: leafEvaluator() - )) + leafEvaluator = mock.MagicMock(side_effect=[True, None]) - leafEvaluator = mock.MagicMock(side_effect=[True, None]) + self.assertStrictTrue(evaluate(['or', conditionA, conditionB], lambda a: leafEvaluator())) - self.assertStrictTrue(evaluate( - ['or', conditionA, conditionB], - lambda a: leafEvaluator() - )) + def test_or_evaluator__returns_null__when_falses_and_null(self): + """ Test that or_evaluator returns null when operands evaluate to falses and null. """ - def test_or_evaluator__returns_null__when_falses_and_null(self): - """ Test that or_evaluator returns null when operands evaluate to falses and null. """ + leafEvaluator = mock.MagicMock(side_effect=[False, None]) - leafEvaluator = mock.MagicMock(side_effect=[False, None]) + self.assertIsNone(evaluate(['or', conditionA, conditionB], lambda a: leafEvaluator())) - self.assertIsNone(evaluate( - ['or', conditionA, conditionB], - lambda a: leafEvaluator() - )) + leafEvaluator = mock.MagicMock(side_effect=[None, False]) - leafEvaluator = mock.MagicMock(side_effect=[None, False]) + self.assertIsNone(evaluate(['or', conditionA, conditionB], lambda a: leafEvaluator())) - self.assertIsNone(evaluate( - ['or', conditionA, conditionB], - lambda a: leafEvaluator() - )) + def test_or_evaluator__returns_true__when_trues_falses_and_null(self): + """ Test that or_evaluator returns True when operands evaluate to trues, falses and null. """ - def test_or_evaluator__returns_true__when_trues_falses_and_null(self): - """ Test that or_evaluator returns True when operands evaluate to trues, falses and null. """ + leafEvaluator = mock.MagicMock(side_effect=[False, None, True]) - leafEvaluator = mock.MagicMock(side_effect=[False, None, True]) + self.assertStrictTrue(evaluate(['or', conditionA, conditionB, conditionC], lambda a: leafEvaluator())) - self.assertStrictTrue(evaluate( - ['or', conditionA, conditionB, conditionC], - lambda a: leafEvaluator() - )) + def test_not_evaluator__returns_true(self): + """ Test that not_evaluator returns True when condition evaluates to False. """ - def test_not_evaluator__returns_true(self): - """ Test that not_evaluator returns True when condition evaluates to False. """ + self.assertStrictTrue(evaluate(['not', conditionA], lambda a: False)) - self.assertStrictTrue(evaluate( - ['not', conditionA], - lambda a: False - )) + def test_not_evaluator__returns_false(self): + """ Test that not_evaluator returns True when condition evaluates to False. """ - def test_not_evaluator__returns_false(self): - """ Test that not_evaluator returns True when condition evaluates to False. """ + self.assertStrictFalse(evaluate(['not', conditionA], lambda a: True)) - self.assertStrictFalse(evaluate( - ['not', conditionA], - lambda a: True - )) + def test_not_evaluator_negates_first_condition__ignores_rest(self): + """ Test that not_evaluator negates first condition and ignores rest. """ + leafEvaluator = mock.MagicMock(side_effect=[False, True, None]) - def test_not_evaluator_negates_first_condition__ignores_rest(self): - """ Test that not_evaluator negates first condition and ignores rest. """ - leafEvaluator = mock.MagicMock(side_effect=[False, True, None]) + self.assertStrictTrue(evaluate(['not', conditionA, conditionB, conditionC], lambda a: leafEvaluator())) - self.assertStrictTrue(evaluate( - ['not', conditionA, conditionB, conditionC], - lambda a: leafEvaluator() - )) + leafEvaluator = mock.MagicMock(side_effect=[True, False, None]) - leafEvaluator = mock.MagicMock(side_effect=[True, False, None]) + self.assertStrictFalse(evaluate(['not', conditionA, conditionB, conditionC], lambda a: leafEvaluator())) - self.assertStrictFalse(evaluate( - ['not', conditionA, conditionB, conditionC], - lambda a: leafEvaluator() - )) + leafEvaluator = mock.MagicMock(side_effect=[None, True, False]) - leafEvaluator = mock.MagicMock(side_effect=[None, True, False]) + self.assertIsNone(evaluate(['not', conditionA, conditionB, conditionC], lambda a: leafEvaluator())) - self.assertIsNone(evaluate( - ['not', conditionA, conditionB, conditionC], - lambda a: leafEvaluator() - )) + def test_not_evaluator__returns_null__when_null(self): + """ Test that not_evaluator returns null when condition evaluates to null. """ - def test_not_evaluator__returns_null__when_null(self): - """ Test that not_evaluator returns null when condition evaluates to null. """ + self.assertIsNone(evaluate(['not', conditionA], lambda a: None)) - self.assertIsNone(evaluate( - ['not', conditionA], - lambda a: None - )) + def test_not_evaluator__returns_null__when_there_are_no_operands(self): + """ Test that not_evaluator returns null when there are no conditions. """ - def test_not_evaluator__returns_null__when_there_are_no_operands(self): - """ Test that not_evaluator returns null when there are no conditions. """ + self.assertIsNone(evaluate(['not'], lambda a: True)) - self.assertIsNone(evaluate( - ['not'], - lambda a: True - )) - - def test_evaluate_assumes__OR_operator__when_first_item_in_array_not_recognized_operator(self): - """ Test that by default OR operator is assumed when the first item in conditions is not + def test_evaluate_assumes__OR_operator__when_first_item_in_array_not_recognized_operator(self,): + """ Test that by default OR operator is assumed when the first item in conditions is not a recognized operator. """ - leafEvaluator = mock.MagicMock(side_effect=[False, True]) + leafEvaluator = mock.MagicMock(side_effect=[False, True]) - self.assertStrictTrue(evaluate( - [conditionA, conditionB], - lambda a: leafEvaluator() - )) + self.assertStrictTrue(evaluate([conditionA, conditionB], lambda a: leafEvaluator())) - self.assertStrictFalse(evaluate( - [conditionA, conditionB], - lambda a: False - )) + self.assertStrictFalse(evaluate([conditionA, conditionB], lambda a: False)) diff --git a/tests/helpers_tests/test_event_tag_utils.py b/tests/helpers_tests/test_event_tag_utils.py index 878a8d24..ae2c8d4c 100644 --- a/tests/helpers_tests/test_event_tag_utils.py +++ b/tests/helpers_tests/test_event_tag_utils.py @@ -19,110 +19,133 @@ class EventTagUtilsTest(unittest.TestCase): - - def test_get_revenue_value__invalid_args(self): - """ Test that revenue value is not returned for invalid arguments. """ - self.assertIsNone(event_tag_utils.get_revenue_value(None)) - self.assertIsNone(event_tag_utils.get_revenue_value(0.5)) - self.assertIsNone(event_tag_utils.get_revenue_value(65536)) - self.assertIsNone(event_tag_utils.get_revenue_value(9223372036854775807)) - self.assertIsNone(event_tag_utils.get_revenue_value('9223372036854775807')) - self.assertIsNone(event_tag_utils.get_revenue_value(True)) - self.assertIsNone(event_tag_utils.get_revenue_value(False)) - - def test_get_revenue_value__no_revenue_tag(self): - """ Test that revenue value is not returned when there's no revenue event tag. """ - self.assertIsNone(event_tag_utils.get_revenue_value([])) - self.assertIsNone(event_tag_utils.get_revenue_value({})) - self.assertIsNone(event_tag_utils.get_revenue_value({'non-revenue': 42})) - - def test_get_revenue_value__invalid_revenue_tag(self): - """ Test that revenue value is not returned when revenue event tag has invalid data type. """ - self.assertIsNone(event_tag_utils.get_revenue_value({'revenue': None})) - self.assertIsNone(event_tag_utils.get_revenue_value({'revenue': 0.5})) - self.assertIsNone(event_tag_utils.get_revenue_value({'revenue': '65536'})) - self.assertIsNone(event_tag_utils.get_revenue_value({'revenue': True})) - self.assertIsNone(event_tag_utils.get_revenue_value({'revenue': False})) - self.assertIsNone(event_tag_utils.get_revenue_value({'revenue': [1, 2, 3]})) - self.assertIsNone(event_tag_utils.get_revenue_value({'revenue': {'a', 'b', 'c'}})) - - def test_get_revenue_value__revenue_tag(self): - """ Test that correct revenue value is returned. """ - self.assertEqual(0, event_tag_utils.get_revenue_value({'revenue': 0})) - self.assertEqual(65536, event_tag_utils.get_revenue_value({'revenue': 65536})) - self.assertEqual(9223372036854775807, event_tag_utils.get_revenue_value({'revenue': 9223372036854775807})) - - def test_get_numeric_metric__invalid_args(self): - """ Test that numeric value is not returned for invalid arguments. """ - self.assertIsNone(event_tag_utils.get_numeric_value(None)) - self.assertIsNone(event_tag_utils.get_numeric_value(0.5)) - self.assertIsNone(event_tag_utils.get_numeric_value(65536)) - self.assertIsNone(event_tag_utils.get_numeric_value(9223372036854775807)) - self.assertIsNone(event_tag_utils.get_numeric_value('9223372036854775807')) - self.assertIsNone(event_tag_utils.get_numeric_value(True)) - self.assertIsNone(event_tag_utils.get_numeric_value(False)) - - def test_get_numeric_metric__no_value_tag(self): - """ Test that numeric value is not returned when there's no numeric event tag. """ - self.assertIsNone(event_tag_utils.get_numeric_value([])) - self.assertIsNone(event_tag_utils.get_numeric_value({})) - self.assertIsNone(event_tag_utils.get_numeric_value({'non-value': 42})) - - def test_get_numeric_metric__invalid_value_tag(self): - """ Test that numeric value is not returned when value event tag has invalid data type. """ - self.assertIsNone(event_tag_utils.get_numeric_value({'value': None})) - self.assertIsNone(event_tag_utils.get_numeric_value({'value': True})) - self.assertIsNone(event_tag_utils.get_numeric_value({'value': False})) - self.assertIsNone(event_tag_utils.get_numeric_value({'value': [1, 2, 3]})) - self.assertIsNone(event_tag_utils.get_numeric_value({'value': {'a', 'b', 'c'}})) - - def test_get_numeric_metric__value_tag(self): - """ Test that the correct numeric value is returned. """ - - # An integer should be cast to a float - self.assertEqual(12345.0, event_tag_utils.get_numeric_value({'value': 12345}, logger=logger.SimpleLogger())) - - # A string should be cast to a float - self.assertEqual(12345.0, event_tag_utils.get_numeric_value({'value': '12345'}, logger=logger.SimpleLogger())) - - # Valid float values - some_float = 1.2345 - self.assertEqual(some_float, event_tag_utils.get_numeric_value({'value': some_float}, logger=logger.SimpleLogger())) - - max_float = sys.float_info.max - self.assertEqual(max_float, event_tag_utils.get_numeric_value({'value': max_float}, logger=logger.SimpleLogger())) - - min_float = sys.float_info.min - self.assertEqual(min_float, event_tag_utils.get_numeric_value({'value': min_float}, logger=logger.SimpleLogger())) - - # Invalid values - self.assertIsNone(event_tag_utils.get_numeric_value({'value': False}, logger=logger.SimpleLogger())) - self.assertIsNone(event_tag_utils.get_numeric_value({'value': None}, logger=logger.SimpleLogger())) - - numeric_value_nan = event_tag_utils.get_numeric_value({'value': float('nan')}, logger=logger.SimpleLogger()) - self.assertIsNone(numeric_value_nan, 'nan numeric value is {}'.format(numeric_value_nan)) - - numeric_value_array = event_tag_utils.get_numeric_value({'value': []}, logger=logger.SimpleLogger()) - self.assertIsNone(numeric_value_array, 'Array numeric value is {}'.format(numeric_value_array)) - - numeric_value_dict = event_tag_utils.get_numeric_value({'value': []}, logger=logger.SimpleLogger()) - self.assertIsNone(numeric_value_dict, 'Dict numeric value is {}'.format(numeric_value_dict)) - - numeric_value_none = event_tag_utils.get_numeric_value({'value': None}, logger=logger.SimpleLogger()) - self.assertIsNone(numeric_value_none, 'None numeric value is {}'.format(numeric_value_none)) - - numeric_value_invalid_literal = event_tag_utils.get_numeric_value({'value': '1,234'}, logger=logger.SimpleLogger()) - self.assertIsNone(numeric_value_invalid_literal, 'Invalid string literal value is {}' - .format(numeric_value_invalid_literal)) - - numeric_value_overflow = event_tag_utils.get_numeric_value({'value': sys.float_info.max * 10}, - logger=logger.SimpleLogger()) - self.assertIsNone(numeric_value_overflow, 'Max numeric value is {}'.format(numeric_value_overflow)) - - numeric_value_inf = event_tag_utils.get_numeric_value({'value': float('inf')}, logger=logger.SimpleLogger()) - self.assertIsNone(numeric_value_inf, 'Infinity numeric value is {}'.format(numeric_value_inf)) - - numeric_value_neg_inf = event_tag_utils.get_numeric_value({'value': float('-inf')}, logger=logger.SimpleLogger()) - self.assertIsNone(numeric_value_neg_inf, 'Negative infinity numeric value is {}'.format(numeric_value_neg_inf)) - - self.assertEqual(0.0, event_tag_utils.get_numeric_value({'value': 0.0}, logger=logger.SimpleLogger())) + def test_get_revenue_value__invalid_args(self): + """ Test that revenue value is not returned for invalid arguments. """ + self.assertIsNone(event_tag_utils.get_revenue_value(None)) + self.assertIsNone(event_tag_utils.get_revenue_value(0.5)) + self.assertIsNone(event_tag_utils.get_revenue_value(65536)) + self.assertIsNone(event_tag_utils.get_revenue_value(9223372036854775807)) + self.assertIsNone(event_tag_utils.get_revenue_value('9223372036854775807')) + self.assertIsNone(event_tag_utils.get_revenue_value(True)) + self.assertIsNone(event_tag_utils.get_revenue_value(False)) + + def test_get_revenue_value__no_revenue_tag(self): + """ Test that revenue value is not returned when there's no revenue event tag. """ + self.assertIsNone(event_tag_utils.get_revenue_value([])) + self.assertIsNone(event_tag_utils.get_revenue_value({})) + self.assertIsNone(event_tag_utils.get_revenue_value({'non-revenue': 42})) + + def test_get_revenue_value__invalid_revenue_tag(self): + """ Test that revenue value is not returned when revenue event tag has invalid data type. """ + self.assertIsNone(event_tag_utils.get_revenue_value({'revenue': None})) + self.assertIsNone(event_tag_utils.get_revenue_value({'revenue': 0.5})) + self.assertIsNone(event_tag_utils.get_revenue_value({'revenue': '65536'})) + self.assertIsNone(event_tag_utils.get_revenue_value({'revenue': True})) + self.assertIsNone(event_tag_utils.get_revenue_value({'revenue': False})) + self.assertIsNone(event_tag_utils.get_revenue_value({'revenue': [1, 2, 3]})) + self.assertIsNone(event_tag_utils.get_revenue_value({'revenue': {'a', 'b', 'c'}})) + + def test_get_revenue_value__revenue_tag(self): + """ Test that correct revenue value is returned. """ + self.assertEqual(0, event_tag_utils.get_revenue_value({'revenue': 0})) + self.assertEqual(65536, event_tag_utils.get_revenue_value({'revenue': 65536})) + self.assertEqual( + 9223372036854775807, event_tag_utils.get_revenue_value({'revenue': 9223372036854775807}), + ) + + def test_get_numeric_metric__invalid_args(self): + """ Test that numeric value is not returned for invalid arguments. """ + self.assertIsNone(event_tag_utils.get_numeric_value(None)) + self.assertIsNone(event_tag_utils.get_numeric_value(0.5)) + self.assertIsNone(event_tag_utils.get_numeric_value(65536)) + self.assertIsNone(event_tag_utils.get_numeric_value(9223372036854775807)) + self.assertIsNone(event_tag_utils.get_numeric_value('9223372036854775807')) + self.assertIsNone(event_tag_utils.get_numeric_value(True)) + self.assertIsNone(event_tag_utils.get_numeric_value(False)) + + def test_get_numeric_metric__no_value_tag(self): + """ Test that numeric value is not returned when there's no numeric event tag. """ + self.assertIsNone(event_tag_utils.get_numeric_value([])) + self.assertIsNone(event_tag_utils.get_numeric_value({})) + self.assertIsNone(event_tag_utils.get_numeric_value({'non-value': 42})) + + def test_get_numeric_metric__invalid_value_tag(self): + """ Test that numeric value is not returned when value event tag has invalid data type. """ + self.assertIsNone(event_tag_utils.get_numeric_value({'value': None})) + self.assertIsNone(event_tag_utils.get_numeric_value({'value': True})) + self.assertIsNone(event_tag_utils.get_numeric_value({'value': False})) + self.assertIsNone(event_tag_utils.get_numeric_value({'value': [1, 2, 3]})) + self.assertIsNone(event_tag_utils.get_numeric_value({'value': {'a', 'b', 'c'}})) + + def test_get_numeric_metric__value_tag(self): + """ Test that the correct numeric value is returned. """ + + # An integer should be cast to a float + self.assertEqual( + 12345.0, event_tag_utils.get_numeric_value({'value': 12345}, logger=logger.SimpleLogger()), + ) + + # A string should be cast to a float + self.assertEqual( + 12345.0, event_tag_utils.get_numeric_value({'value': '12345'}, logger=logger.SimpleLogger()), + ) + + # Valid float values + some_float = 1.2345 + self.assertEqual( + some_float, event_tag_utils.get_numeric_value({'value': some_float}, logger=logger.SimpleLogger()), + ) + + max_float = sys.float_info.max + self.assertEqual( + max_float, event_tag_utils.get_numeric_value({'value': max_float}, logger=logger.SimpleLogger()), + ) + + min_float = sys.float_info.min + self.assertEqual( + min_float, event_tag_utils.get_numeric_value({'value': min_float}, logger=logger.SimpleLogger()), + ) + + # Invalid values + self.assertIsNone(event_tag_utils.get_numeric_value({'value': False}, logger=logger.SimpleLogger())) + self.assertIsNone(event_tag_utils.get_numeric_value({'value': None}, logger=logger.SimpleLogger())) + + numeric_value_nan = event_tag_utils.get_numeric_value({'value': float('nan')}, logger=logger.SimpleLogger()) + self.assertIsNone(numeric_value_nan, 'nan numeric value is {}'.format(numeric_value_nan)) + + numeric_value_array = event_tag_utils.get_numeric_value({'value': []}, logger=logger.SimpleLogger()) + self.assertIsNone(numeric_value_array, 'Array numeric value is {}'.format(numeric_value_array)) + + numeric_value_dict = event_tag_utils.get_numeric_value({'value': []}, logger=logger.SimpleLogger()) + self.assertIsNone(numeric_value_dict, 'Dict numeric value is {}'.format(numeric_value_dict)) + + numeric_value_none = event_tag_utils.get_numeric_value({'value': None}, logger=logger.SimpleLogger()) + self.assertIsNone(numeric_value_none, 'None numeric value is {}'.format(numeric_value_none)) + + numeric_value_invalid_literal = event_tag_utils.get_numeric_value( + {'value': '1,234'}, logger=logger.SimpleLogger() + ) + self.assertIsNone( + numeric_value_invalid_literal, 'Invalid string literal value is {}'.format(numeric_value_invalid_literal), + ) + + numeric_value_overflow = event_tag_utils.get_numeric_value( + {'value': sys.float_info.max * 10}, logger=logger.SimpleLogger() + ) + self.assertIsNone( + numeric_value_overflow, 'Max numeric value is {}'.format(numeric_value_overflow), + ) + + numeric_value_inf = event_tag_utils.get_numeric_value({'value': float('inf')}, logger=logger.SimpleLogger()) + self.assertIsNone(numeric_value_inf, 'Infinity numeric value is {}'.format(numeric_value_inf)) + + numeric_value_neg_inf = event_tag_utils.get_numeric_value( + {'value': float('-inf')}, logger=logger.SimpleLogger() + ) + self.assertIsNone( + numeric_value_neg_inf, 'Negative infinity numeric value is {}'.format(numeric_value_neg_inf), + ) + + self.assertEqual( + 0.0, event_tag_utils.get_numeric_value({'value': 0.0}, logger=logger.SimpleLogger()), + ) diff --git a/tests/helpers_tests/test_experiment.py b/tests/helpers_tests/test_experiment.py index fd46f3b4..58f9b6d8 100644 --- a/tests/helpers_tests/test_experiment.py +++ b/tests/helpers_tests/test_experiment.py @@ -19,17 +19,21 @@ class ExperimentTest(base.BaseTest): - - def test_is_experiment_running__status_running(self): - """ Test that is_experiment_running returns True when experiment has Running status. """ - - self.assertTrue(experiment.is_experiment_running(self.project_config.get_experiment_from_key('test_experiment'))) - - def test_is_experiment_running__status_not_running(self): - """ Test that is_experiment_running returns False when experiment does not have running status. """ - - with mock.patch('optimizely.project_config.ProjectConfig.get_experiment_from_key', - return_value=entities.Experiment( - '42', 'test_experiment', 'Some Status', [], [], {}, [], '43')) as mock_get_experiment: - self.assertFalse(experiment.is_experiment_running(self.project_config.get_experiment_from_key('test_experiment'))) - mock_get_experiment.assert_called_once_with('test_experiment') + def test_is_experiment_running__status_running(self): + """ Test that is_experiment_running returns True when experiment has Running status. """ + + self.assertTrue( + experiment.is_experiment_running(self.project_config.get_experiment_from_key('test_experiment')) + ) + + def test_is_experiment_running__status_not_running(self): + """ Test that is_experiment_running returns False when experiment does not have running status. """ + + with mock.patch( + 'optimizely.project_config.ProjectConfig.get_experiment_from_key', + return_value=entities.Experiment('42', 'test_experiment', 'Some Status', [], [], {}, [], '43'), + ) as mock_get_experiment: + self.assertFalse( + experiment.is_experiment_running(self.project_config.get_experiment_from_key('test_experiment')) + ) + mock_get_experiment.assert_called_once_with('test_experiment') diff --git a/tests/helpers_tests/test_validator.py b/tests/helpers_tests/test_validator.py index 8d390fdd..f27b45a3 100644 --- a/tests/helpers_tests/test_validator.py +++ b/tests/helpers_tests/test_validator.py @@ -27,249 +27,260 @@ class ValidatorTest(base.BaseTest): + def test_is_config_manager_valid__returns_true(self): + """ Test that valid config_manager returns True for valid config manager implementation. """ - def test_is_config_manager_valid__returns_true(self): - """ Test that valid config_manager returns True for valid config manager implementation. """ + self.assertTrue(validator.is_config_manager_valid(config_manager.StaticConfigManager)) + self.assertTrue(validator.is_config_manager_valid(config_manager.PollingConfigManager)) - self.assertTrue(validator.is_config_manager_valid(config_manager.StaticConfigManager)) - self.assertTrue(validator.is_config_manager_valid(config_manager.PollingConfigManager)) + def test_is_config_manager_valid__returns_false(self): + """ Test that invalid config_manager returns False for invalid config manager implementation. """ - def test_is_config_manager_valid__returns_false(self): - """ Test that invalid config_manager returns False for invalid config manager implementation. """ + class CustomConfigManager(object): + def some_other_method(self): + pass - class CustomConfigManager(object): - def some_other_method(self): - pass + self.assertFalse(validator.is_config_manager_valid(CustomConfigManager())) - self.assertFalse(validator.is_config_manager_valid(CustomConfigManager())) + def test_is_event_processor_valid__returns_true(self): + """ Test that valid event_processor returns True. """ - def test_is_event_processor_valid__returns_true(self): - """ Test that valid event_processor returns True. """ + self.assertTrue(validator.is_event_processor_valid(event_processor.ForwardingEventProcessor)) - self.assertTrue(validator.is_event_processor_valid(event_processor.ForwardingEventProcessor)) + def test_is_event_processor_valid__returns_false(self): + """ Test that invalid event_processor returns False. """ - def test_is_event_processor_valid__returns_false(self): - """ Test that invalid event_processor returns False. """ + class CustomEventProcessor(object): + def some_other_method(self): + pass - class CustomEventProcessor(object): - def some_other_method(self): - pass + self.assertFalse(validator.is_event_processor_valid(CustomEventProcessor)) - self.assertFalse(validator.is_event_processor_valid(CustomEventProcessor)) + def test_is_datafile_valid__returns_true(self): + """ Test that valid datafile returns True. """ - def test_is_datafile_valid__returns_true(self): - """ Test that valid datafile returns True. """ + self.assertTrue(validator.is_datafile_valid(json.dumps(self.config_dict))) - self.assertTrue(validator.is_datafile_valid(json.dumps(self.config_dict))) + def test_is_datafile_valid__returns_false(self): + """ Test that invalid datafile returns False. """ - def test_is_datafile_valid__returns_false(self): - """ Test that invalid datafile returns False. """ + self.assertFalse(validator.is_datafile_valid(json.dumps({'invalid_key': 'invalid_value'}))) - self.assertFalse(validator.is_datafile_valid(json.dumps({ - 'invalid_key': 'invalid_value' - }))) + def test_is_event_dispatcher_valid__returns_true(self): + """ Test that valid event_dispatcher returns True. """ - def test_is_event_dispatcher_valid__returns_true(self): - """ Test that valid event_dispatcher returns True. """ + self.assertTrue(validator.is_event_dispatcher_valid(event_dispatcher.EventDispatcher)) - self.assertTrue(validator.is_event_dispatcher_valid(event_dispatcher.EventDispatcher)) + def test_is_event_dispatcher_valid__returns_false(self): + """ Test that invalid event_dispatcher returns False. """ - def test_is_event_dispatcher_valid__returns_false(self): - """ Test that invalid event_dispatcher returns False. """ + class CustomEventDispatcher(object): + def some_other_method(self): + pass - class CustomEventDispatcher(object): - def some_other_method(self): - pass + self.assertFalse(validator.is_event_dispatcher_valid(CustomEventDispatcher)) - self.assertFalse(validator.is_event_dispatcher_valid(CustomEventDispatcher)) + def test_is_logger_valid__returns_true(self): + """ Test that valid logger returns True. """ - def test_is_logger_valid__returns_true(self): - """ Test that valid logger returns True. """ + self.assertTrue(validator.is_logger_valid(logger.NoOpLogger)) - self.assertTrue(validator.is_logger_valid(logger.NoOpLogger)) + def test_is_logger_valid__returns_false(self): + """ Test that invalid logger returns False. """ - def test_is_logger_valid__returns_false(self): - """ Test that invalid logger returns False. """ + class CustomLogger(object): + def some_other_method(self): + pass - class CustomLogger(object): - def some_other_method(self): - pass + self.assertFalse(validator.is_logger_valid(CustomLogger)) - self.assertFalse(validator.is_logger_valid(CustomLogger)) + def test_is_error_handler_valid__returns_true(self): + """ Test that valid error_handler returns True. """ - def test_is_error_handler_valid__returns_true(self): - """ Test that valid error_handler returns True. """ + self.assertTrue(validator.is_error_handler_valid(error_handler.NoOpErrorHandler)) - self.assertTrue(validator.is_error_handler_valid(error_handler.NoOpErrorHandler)) + def test_is_error_handler_valid__returns_false(self): + """ Test that invalid error_handler returns False. """ - def test_is_error_handler_valid__returns_false(self): - """ Test that invalid error_handler returns False. """ + class CustomErrorHandler(object): + def some_other_method(self): + pass - class CustomErrorHandler(object): - def some_other_method(self): - pass + self.assertFalse(validator.is_error_handler_valid(CustomErrorHandler)) - self.assertFalse(validator.is_error_handler_valid(CustomErrorHandler)) + def test_are_attributes_valid__returns_true(self): + """ Test that valid attributes returns True. """ - def test_are_attributes_valid__returns_true(self): - """ Test that valid attributes returns True. """ + self.assertTrue(validator.are_attributes_valid({'key': 'value'})) - self.assertTrue(validator.are_attributes_valid({'key': 'value'})) + def test_are_attributes_valid__returns_false(self): + """ Test that invalid attributes returns False. """ - def test_are_attributes_valid__returns_false(self): - """ Test that invalid attributes returns False. """ + self.assertFalse(validator.are_attributes_valid('key:value')) + self.assertFalse(validator.are_attributes_valid(['key', 'value'])) + self.assertFalse(validator.are_attributes_valid(42)) - self.assertFalse(validator.are_attributes_valid('key:value')) - self.assertFalse(validator.are_attributes_valid(['key', 'value'])) - self.assertFalse(validator.are_attributes_valid(42)) + def test_are_event_tags_valid__returns_true(self): + """ Test that valid event tags returns True. """ - def test_are_event_tags_valid__returns_true(self): - """ Test that valid event tags returns True. """ + self.assertTrue(validator.are_event_tags_valid({'key': 'value', 'revenue': 0})) - self.assertTrue(validator.are_event_tags_valid({'key': 'value', 'revenue': 0})) + def test_are_event_tags_valid__returns_false(self): + """ Test that invalid event tags returns False. """ - def test_are_event_tags_valid__returns_false(self): - """ Test that invalid event tags returns False. """ + self.assertFalse(validator.are_event_tags_valid('key:value')) + self.assertFalse(validator.are_event_tags_valid(['key', 'value'])) + self.assertFalse(validator.are_event_tags_valid(42)) - self.assertFalse(validator.are_event_tags_valid('key:value')) - self.assertFalse(validator.are_event_tags_valid(['key', 'value'])) - self.assertFalse(validator.are_event_tags_valid(42)) + def test_is_user_profile_valid__returns_true(self): + """ Test that valid user profile returns True. """ - def test_is_user_profile_valid__returns_true(self): - """ Test that valid user profile returns True. """ - - self.assertTrue(validator.is_user_profile_valid({'user_id': 'test_user', 'experiment_bucket_map': {}})) - self.assertTrue(validator.is_user_profile_valid({'user_id': 'test_user', - 'experiment_bucket_map': {'1234': {'variation_id': '5678'}}})) - self.assertTrue(validator.is_user_profile_valid({'user_id': 'test_user', - 'experiment_bucket_map': {'1234': {'variation_id': '5678'}}, - 'additional_key': 'additional_value'})) - self.assertTrue(validator.is_user_profile_valid({'user_id': 'test_user', - 'experiment_bucket_map': {'1234': - {'variation_id': '5678', - 'additional_key': 'additional_value'} - }})) - - def test_is_user_profile_valid__returns_false(self): - """ Test that invalid user profile returns True. """ - - self.assertFalse(validator.is_user_profile_valid(None)) - self.assertFalse(validator.is_user_profile_valid('user_id')) - self.assertFalse(validator.is_user_profile_valid({'some_key': 'some_value'})) - self.assertFalse(validator.is_user_profile_valid({'user_id': 'test_user'})) - self.assertFalse(validator.is_user_profile_valid({'user_id': 'test_user', 'experiment_bucket_map': 'some_value'})) - self.assertFalse(validator.is_user_profile_valid({'user_id': 'test_user', - 'experiment_bucket_map': {'1234': 'some_value'}})) - self.assertFalse(validator.is_user_profile_valid({'user_id': 'test_user', - 'experiment_bucket_map': {'1234': {'variation_id': '5678'}, - '1235': {'some_key': 'some_value'}}})) - - def test_is_non_empty_string(self): - """ Test that the method returns True only for a non-empty string. """ - - self.assertFalse(validator.is_non_empty_string(None)) - self.assertFalse(validator.is_non_empty_string([])) - self.assertFalse(validator.is_non_empty_string({})) - self.assertFalse(validator.is_non_empty_string(0)) - self.assertFalse(validator.is_non_empty_string(99)) - self.assertFalse(validator.is_non_empty_string(1.2)) - self.assertFalse(validator.is_non_empty_string(True)) - self.assertFalse(validator.is_non_empty_string(False)) - self.assertFalse(validator.is_non_empty_string('')) - - self.assertTrue(validator.is_non_empty_string('0')) - self.assertTrue(validator.is_non_empty_string('test_user')) - - def test_is_attribute_valid(self): - """ Test that non-string attribute key or unsupported attribute value returns False.""" - - # test invalid attribute keys - self.assertFalse(validator.is_attribute_valid(5, 'test_value')) - self.assertFalse(validator.is_attribute_valid(True, 'test_value')) - self.assertFalse(validator.is_attribute_valid(5.5, 'test_value')) - - # test invalid attribute values - self.assertFalse(validator.is_attribute_valid('test_attribute', None)) - self.assertFalse(validator.is_attribute_valid('test_attribute', {})) - self.assertFalse(validator.is_attribute_valid('test_attribute', [])) - self.assertFalse(validator.is_attribute_valid('test_attribute', ())) - - # test valid attribute values - self.assertTrue(validator.is_attribute_valid('test_attribute', False)) - self.assertTrue(validator.is_attribute_valid('test_attribute', True)) - self.assertTrue(validator.is_attribute_valid('test_attribute', 0)) - self.assertTrue(validator.is_attribute_valid('test_attribute', 0.0)) - self.assertTrue(validator.is_attribute_valid('test_attribute', "")) - self.assertTrue(validator.is_attribute_valid('test_attribute', 'test_value')) - - # test if attribute value is a number, it calls is_finite_number and returns it's result - with mock.patch('optimizely.helpers.validator.is_finite_number', - return_value=True) as mock_is_finite: - self.assertTrue(validator.is_attribute_valid('test_attribute', 5)) - - mock_is_finite.assert_called_once_with(5) - - with mock.patch('optimizely.helpers.validator.is_finite_number', - return_value=False) as mock_is_finite: - self.assertFalse(validator.is_attribute_valid('test_attribute', 5.5)) - - mock_is_finite.assert_called_once_with(5.5) - - if PY2: - with mock.patch('optimizely.helpers.validator.is_finite_number', - return_value=None) as mock_is_finite: - self.assertIsNone(validator.is_attribute_valid('test_attribute', long(5))) - - mock_is_finite.assert_called_once_with(long(5)) - - def test_is_finite_number(self): - """ Test that it returns true if value is a number and not NAN, INF, -INF or greater than 2^53. + self.assertTrue(validator.is_user_profile_valid({'user_id': 'test_user', 'experiment_bucket_map': {}})) + self.assertTrue( + validator.is_user_profile_valid( + {'user_id': 'test_user', 'experiment_bucket_map': {'1234': {'variation_id': '5678'}}} + ) + ) + self.assertTrue( + validator.is_user_profile_valid( + { + 'user_id': 'test_user', + 'experiment_bucket_map': {'1234': {'variation_id': '5678'}}, + 'additional_key': 'additional_value', + } + ) + ) + self.assertTrue( + validator.is_user_profile_valid( + { + 'user_id': 'test_user', + 'experiment_bucket_map': {'1234': {'variation_id': '5678', 'additional_key': 'additional_value'}}, + } + ) + ) + + def test_is_user_profile_valid__returns_false(self): + """ Test that invalid user profile returns True. """ + + self.assertFalse(validator.is_user_profile_valid(None)) + self.assertFalse(validator.is_user_profile_valid('user_id')) + self.assertFalse(validator.is_user_profile_valid({'some_key': 'some_value'})) + self.assertFalse(validator.is_user_profile_valid({'user_id': 'test_user'})) + self.assertFalse( + validator.is_user_profile_valid({'user_id': 'test_user', 'experiment_bucket_map': 'some_value'}) + ) + self.assertFalse( + validator.is_user_profile_valid({'user_id': 'test_user', 'experiment_bucket_map': {'1234': 'some_value'}}) + ) + self.assertFalse( + validator.is_user_profile_valid( + { + 'user_id': 'test_user', + 'experiment_bucket_map': {'1234': {'variation_id': '5678'}, '1235': {'some_key': 'some_value'}}, + } + ) + ) + + def test_is_non_empty_string(self): + """ Test that the method returns True only for a non-empty string. """ + + self.assertFalse(validator.is_non_empty_string(None)) + self.assertFalse(validator.is_non_empty_string([])) + self.assertFalse(validator.is_non_empty_string({})) + self.assertFalse(validator.is_non_empty_string(0)) + self.assertFalse(validator.is_non_empty_string(99)) + self.assertFalse(validator.is_non_empty_string(1.2)) + self.assertFalse(validator.is_non_empty_string(True)) + self.assertFalse(validator.is_non_empty_string(False)) + self.assertFalse(validator.is_non_empty_string('')) + + self.assertTrue(validator.is_non_empty_string('0')) + self.assertTrue(validator.is_non_empty_string('test_user')) + + def test_is_attribute_valid(self): + """ Test that non-string attribute key or unsupported attribute value returns False.""" + + # test invalid attribute keys + self.assertFalse(validator.is_attribute_valid(5, 'test_value')) + self.assertFalse(validator.is_attribute_valid(True, 'test_value')) + self.assertFalse(validator.is_attribute_valid(5.5, 'test_value')) + + # test invalid attribute values + self.assertFalse(validator.is_attribute_valid('test_attribute', None)) + self.assertFalse(validator.is_attribute_valid('test_attribute', {})) + self.assertFalse(validator.is_attribute_valid('test_attribute', [])) + self.assertFalse(validator.is_attribute_valid('test_attribute', ())) + + # test valid attribute values + self.assertTrue(validator.is_attribute_valid('test_attribute', False)) + self.assertTrue(validator.is_attribute_valid('test_attribute', True)) + self.assertTrue(validator.is_attribute_valid('test_attribute', 0)) + self.assertTrue(validator.is_attribute_valid('test_attribute', 0.0)) + self.assertTrue(validator.is_attribute_valid('test_attribute', "")) + self.assertTrue(validator.is_attribute_valid('test_attribute', 'test_value')) + + # test if attribute value is a number, it calls is_finite_number and returns it's result + with mock.patch('optimizely.helpers.validator.is_finite_number', return_value=True) as mock_is_finite: + self.assertTrue(validator.is_attribute_valid('test_attribute', 5)) + + mock_is_finite.assert_called_once_with(5) + + with mock.patch('optimizely.helpers.validator.is_finite_number', return_value=False) as mock_is_finite: + self.assertFalse(validator.is_attribute_valid('test_attribute', 5.5)) + + mock_is_finite.assert_called_once_with(5.5) + + if PY2: + with mock.patch('optimizely.helpers.validator.is_finite_number', return_value=None) as mock_is_finite: + self.assertIsNone(validator.is_attribute_valid('test_attribute', long(5))) + + mock_is_finite.assert_called_once_with(long(5)) + + def test_is_finite_number(self): + """ Test that it returns true if value is a number and not NAN, INF, -INF or greater than 2^53. Otherwise False. """ - # test non number values - self.assertFalse(validator.is_finite_number('HelloWorld')) - self.assertFalse(validator.is_finite_number(True)) - self.assertFalse(validator.is_finite_number(False)) - self.assertFalse(validator.is_finite_number(None)) - self.assertFalse(validator.is_finite_number({})) - self.assertFalse(validator.is_finite_number([])) - self.assertFalse(validator.is_finite_number(())) - - # test invalid numbers - self.assertFalse(validator.is_finite_number(float('inf'))) - self.assertFalse(validator.is_finite_number(float('-inf'))) - self.assertFalse(validator.is_finite_number(float('nan'))) - self.assertFalse(validator.is_finite_number(int(2**53) + 1)) - self.assertFalse(validator.is_finite_number(-int(2**53) - 1)) - self.assertFalse(validator.is_finite_number(float(2**53) + 2.0)) - self.assertFalse(validator.is_finite_number(-float(2**53) - 2.0)) - if PY2: - self.assertFalse(validator.is_finite_number(long(2**53) + 1)) - self.assertFalse(validator.is_finite_number(-long(2**53) - 1)) - - # test valid numbers - self.assertTrue(validator.is_finite_number(0)) - self.assertTrue(validator.is_finite_number(5)) - self.assertTrue(validator.is_finite_number(5.5)) - # float(2**53) + 1.0 evaluates to float(2**53) - self.assertTrue(validator.is_finite_number(float(2**53) + 1.0)) - self.assertTrue(validator.is_finite_number(-float(2**53) - 1.0)) - self.assertTrue(validator.is_finite_number(int(2**53))) - if PY2: - self.assertTrue(validator.is_finite_number(long(2**53))) + # test non number values + self.assertFalse(validator.is_finite_number('HelloWorld')) + self.assertFalse(validator.is_finite_number(True)) + self.assertFalse(validator.is_finite_number(False)) + self.assertFalse(validator.is_finite_number(None)) + self.assertFalse(validator.is_finite_number({})) + self.assertFalse(validator.is_finite_number([])) + self.assertFalse(validator.is_finite_number(())) + + # test invalid numbers + self.assertFalse(validator.is_finite_number(float('inf'))) + self.assertFalse(validator.is_finite_number(float('-inf'))) + self.assertFalse(validator.is_finite_number(float('nan'))) + self.assertFalse(validator.is_finite_number(int(2 ** 53) + 1)) + self.assertFalse(validator.is_finite_number(-int(2 ** 53) - 1)) + self.assertFalse(validator.is_finite_number(float(2 ** 53) + 2.0)) + self.assertFalse(validator.is_finite_number(-float(2 ** 53) - 2.0)) + if PY2: + self.assertFalse(validator.is_finite_number(long(2 ** 53) + 1)) + self.assertFalse(validator.is_finite_number(-long(2 ** 53) - 1)) + + # test valid numbers + self.assertTrue(validator.is_finite_number(0)) + self.assertTrue(validator.is_finite_number(5)) + self.assertTrue(validator.is_finite_number(5.5)) + # float(2**53) + 1.0 evaluates to float(2**53) + self.assertTrue(validator.is_finite_number(float(2 ** 53) + 1.0)) + self.assertTrue(validator.is_finite_number(-float(2 ** 53) - 1.0)) + self.assertTrue(validator.is_finite_number(int(2 ** 53))) + if PY2: + self.assertTrue(validator.is_finite_number(long(2 ** 53))) class DatafileValidationTests(base.BaseTest): + def test_is_datafile_valid__returns_true(self): + """ Test that valid datafile returns True. """ - def test_is_datafile_valid__returns_true(self): - """ Test that valid datafile returns True. """ - - self.assertTrue(validator.is_datafile_valid(json.dumps(self.config_dict))) + self.assertTrue(validator.is_datafile_valid(json.dumps(self.config_dict))) - def test_is_datafile_valid__returns_false(self): - """ Test that invalid datafile returns False. """ + def test_is_datafile_valid__returns_false(self): + """ Test that invalid datafile returns False. """ - # When schema is not valid - self.assertFalse(validator.is_datafile_valid(json.dumps({ - 'invalid_key': 'invalid_value' - }))) + # When schema is not valid + self.assertFalse(validator.is_datafile_valid(json.dumps({'invalid_key': 'invalid_value'}))) diff --git a/tests/test_bucketing.py b/tests/test_bucketing.py index 6394dfc6..783c23e2 100644 --- a/tests/test_bucketing.py +++ b/tests/test_bucketing.py @@ -26,292 +26,379 @@ class BucketerTest(base.BaseTest): + def setUp(self, *args, **kwargs): + base.BaseTest.setUp(self) + self.bucketer = bucketer.Bucketer() + + def test_bucket(self): + """ Test that for provided bucket value correct variation ID is returned. """ + + # Variation 1 + with mock.patch( + 'optimizely.bucketer.Bucketer._generate_bucket_value', return_value=42 + ) as mock_generate_bucket_value: + self.assertEqual( + entities.Variation('111128', 'control'), + self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + 'test_user', + 'test_user', + ), + ) + mock_generate_bucket_value.assert_called_once_with('test_user111127') + + # Empty entity ID + with mock.patch( + 'optimizely.bucketer.Bucketer._generate_bucket_value', return_value=4242 + ) as mock_generate_bucket_value: + self.assertIsNone( + self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + 'test_user', + 'test_user', + ) + ) + mock_generate_bucket_value.assert_called_once_with('test_user111127') + + # Variation 2 + with mock.patch( + 'optimizely.bucketer.Bucketer._generate_bucket_value', return_value=5042 + ) as mock_generate_bucket_value: + self.assertEqual( + entities.Variation('111129', 'variation'), + self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + 'test_user', + 'test_user', + ), + ) + mock_generate_bucket_value.assert_called_once_with('test_user111127') + + # No matching variation + with mock.patch( + 'optimizely.bucketer.Bucketer._generate_bucket_value', return_value=424242 + ) as mock_generate_bucket_value: + self.assertIsNone( + self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + 'test_user', + 'test_user', + ) + ) + mock_generate_bucket_value.assert_called_once_with('test_user111127') + + def test_bucket__invalid_experiment(self): + """ Test that bucket returns None for unknown experiment. """ + + self.assertIsNone( + self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('invalid_experiment'), + 'test_user', + 'test_user', + ) + ) + + def test_bucket__invalid_group(self): + """ Test that bucket returns None for unknown group. """ + + project_config = self.project_config + experiment = project_config.get_experiment_from_key('group_exp_1') + # Set invalid group ID for the experiment + experiment.groupId = 'invalid_group_id' + + self.assertIsNone(self.bucketer.bucket(self.project_config, experiment, 'test_user', 'test_user')) + + def test_bucket__experiment_in_group(self): + """ Test that for provided bucket values correct variation ID is returned. """ + + # In group, matching experiment and variation + with mock.patch( + 'optimizely.bucketer.Bucketer._generate_bucket_value', side_effect=[42, 4242], + ) as mock_generate_bucket_value: + self.assertEqual( + entities.Variation('28902', 'group_exp_1_variation'), + self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('group_exp_1'), + 'test_user', + 'test_user', + ), + ) + + self.assertEqual( + [mock.call('test_user19228'), mock.call('test_user32222')], mock_generate_bucket_value.call_args_list, + ) + + # In group, no matching experiment + with mock.patch( + 'optimizely.bucketer.Bucketer._generate_bucket_value', side_effect=[42, 9500], + ) as mock_generate_bucket_value: + self.assertIsNone( + self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('group_exp_1'), + 'test_user', + 'test_user', + ) + ) + self.assertEqual( + [mock.call('test_user19228'), mock.call('test_user32222')], mock_generate_bucket_value.call_args_list, + ) + + # In group, experiment does not match + with mock.patch( + 'optimizely.bucketer.Bucketer._generate_bucket_value', side_effect=[42, 4242], + ) as mock_generate_bucket_value: + self.assertIsNone( + self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('group_exp_2'), + 'test_user', + 'test_user', + ) + ) + mock_generate_bucket_value.assert_called_once_with('test_user19228') + + # In group no matching variation + with mock.patch( + 'optimizely.bucketer.Bucketer._generate_bucket_value', side_effect=[42, 424242], + ) as mock_generate_bucket_value: + self.assertIsNone( + self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('group_exp_1'), + 'test_user', + 'test_user', + ) + ) + self.assertEqual( + [mock.call('test_user19228'), mock.call('test_user32222')], mock_generate_bucket_value.call_args_list, + ) + + def test_bucket_number(self): + """ Test output of _generate_bucket_value for different inputs. """ + + def get_bucketing_id(bucketing_id, parent_id=None): + parent_id = parent_id or 1886780721 + return bucketer.BUCKETING_ID_TEMPLATE.format(bucketing_id=bucketing_id, parent_id=parent_id) + + self.assertEqual(5254, self.bucketer._generate_bucket_value(get_bucketing_id('ppid1'))) + self.assertEqual(4299, self.bucketer._generate_bucket_value(get_bucketing_id('ppid2'))) + self.assertEqual( + 2434, self.bucketer._generate_bucket_value(get_bucketing_id('ppid2', 1886780722)), + ) + self.assertEqual(5439, self.bucketer._generate_bucket_value(get_bucketing_id('ppid3'))) + self.assertEqual( + 6128, + self.bucketer._generate_bucket_value( + get_bucketing_id( + 'a very very very very very very very very very very very very very very very long ppd string' + ) + ), + ) + + def test_hash_values(self): + """ Test that on randomized data, values computed from mmh3 and pymmh3 match. """ - def setUp(self, *args, **kwargs): - base.BaseTest.setUp(self) - self.bucketer = bucketer.Bucketer() - - def test_bucket(self): - """ Test that for provided bucket value correct variation ID is returned. """ - - # Variation 1 - with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', - return_value=42) as mock_generate_bucket_value: - self.assertEqual( - entities.Variation('111128', 'control'), - self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), - 'test_user', 'test_user' - )) - mock_generate_bucket_value.assert_called_once_with('test_user111127') - - # Empty entity ID - with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', - return_value=4242) as mock_generate_bucket_value: - self.assertIsNone(self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), 'test_user', 'test_user' - )) - mock_generate_bucket_value.assert_called_once_with('test_user111127') - - # Variation 2 - with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', - return_value=5042) as mock_generate_bucket_value: - self.assertEqual( - entities.Variation('111129', 'variation'), - self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), 'test_user', 'test_user' - )) - mock_generate_bucket_value.assert_called_once_with('test_user111127') - - # No matching variation - with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', - return_value=424242) as mock_generate_bucket_value: - self.assertIsNone(self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), - 'test_user', - 'test_user') - ) - mock_generate_bucket_value.assert_called_once_with('test_user111127') - - def test_bucket__invalid_experiment(self): - """ Test that bucket returns None for unknown experiment. """ - - self.assertIsNone(self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('invalid_experiment'), - 'test_user', - 'test_user') - ) - - def test_bucket__invalid_group(self): - """ Test that bucket returns None for unknown group. """ - - project_config = self.project_config - experiment = project_config.get_experiment_from_key('group_exp_1') - # Set invalid group ID for the experiment - experiment.groupId = 'invalid_group_id' - - self.assertIsNone(self.bucketer.bucket( - self.project_config, - experiment, - 'test_user', - 'test_user') - ) - - def test_bucket__experiment_in_group(self): - """ Test that for provided bucket values correct variation ID is returned. """ - - # In group, matching experiment and variation - with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', - side_effect=[42, 4242]) as mock_generate_bucket_value: - self.assertEqual(entities.Variation('28902', 'group_exp_1_variation'), - self.bucketer.bucket(self.project_config, - self.project_config.get_experiment_from_key('group_exp_1'), - 'test_user', - 'test_user')) - - self.assertEqual([mock.call('test_user19228'), mock.call('test_user32222')], - mock_generate_bucket_value.call_args_list) - - # In group, no matching experiment - with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', - side_effect=[42, 9500]) as mock_generate_bucket_value: - self.assertIsNone(self.bucketer.bucket(self.project_config, - self.project_config.get_experiment_from_key('group_exp_1'), - 'test_user', - 'test_user')) - self.assertEqual([mock.call('test_user19228'), mock.call('test_user32222')], - mock_generate_bucket_value.call_args_list) - - # In group, experiment does not match - with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', - side_effect=[42, 4242]) as mock_generate_bucket_value: - self.assertIsNone(self.bucketer.bucket(self.project_config, - self.project_config.get_experiment_from_key('group_exp_2'), - 'test_user', - 'test_user')) - mock_generate_bucket_value.assert_called_once_with('test_user19228') - - # In group no matching variation - with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', - side_effect=[42, 424242]) as mock_generate_bucket_value: - self.assertIsNone(self.bucketer.bucket(self.project_config, - self.project_config.get_experiment_from_key('group_exp_1'), - 'test_user', - 'test_user')) - self.assertEqual([mock.call('test_user19228'), mock.call('test_user32222')], - mock_generate_bucket_value.call_args_list) - - def test_bucket_number(self): - """ Test output of _generate_bucket_value for different inputs. """ - - def get_bucketing_id(bucketing_id, parent_id=None): - parent_id = parent_id or 1886780721 - return bucketer.BUCKETING_ID_TEMPLATE.format(bucketing_id=bucketing_id, parent_id=parent_id) - - self.assertEqual(5254, self.bucketer._generate_bucket_value(get_bucketing_id('ppid1'))) - self.assertEqual(4299, self.bucketer._generate_bucket_value(get_bucketing_id('ppid2'))) - self.assertEqual(2434, self.bucketer._generate_bucket_value(get_bucketing_id('ppid2', 1886780722))) - self.assertEqual(5439, self.bucketer._generate_bucket_value(get_bucketing_id('ppid3'))) - self.assertEqual(6128, self.bucketer._generate_bucket_value(get_bucketing_id( - 'a very very very very very very very very very very very very very very very long ppd string'))) - - def test_hash_values(self): - """ Test that on randomized data, values computed from mmh3 and pymmh3 match. """ - - for i in range(10): - random_value = str(random.random()) - self.assertEqual(mmh3.hash(random_value), pymmh3.hash(random_value)) + for i in range(10): + random_value = str(random.random()) + self.assertEqual(mmh3.hash(random_value), pymmh3.hash(random_value)) class BucketerWithLoggingTest(base.BaseTest): - def setUp(self, *args, **kwargs): - base.BaseTest.setUp(self) - self.optimizely = optimizely.Optimizely(json.dumps(self.config_dict), - logger=logger.SimpleLogger()) - self.bucketer = bucketer.Bucketer() - - def test_bucket(self): - """ Test that expected log messages are logged during bucketing. """ - - # Variation 1 - with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', return_value=42),\ - mock.patch.object(self.project_config, 'logger') as mock_config_logging: - self.assertEqual( - entities.Variation('111128', 'control'), - self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), - 'test_user', - 'test_user' + def setUp(self, *args, **kwargs): + base.BaseTest.setUp(self) + self.optimizely = optimizely.Optimizely(json.dumps(self.config_dict), logger=logger.SimpleLogger()) + self.bucketer = bucketer.Bucketer() + + def test_bucket(self): + """ Test that expected log messages are logged during bucketing. """ + + # Variation 1 + with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', return_value=42), mock.patch.object( + self.project_config, 'logger' + ) as mock_config_logging: + self.assertEqual( + entities.Variation('111128', 'control'), + self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + 'test_user', + 'test_user', + ), + ) + + mock_config_logging.debug.assert_called_once_with('Assigned bucket 42 to user with bucketing ID "test_user".') + mock_config_logging.info.assert_called_once_with( + 'User "test_user" is in variation "control" of experiment test_experiment.' + ) + + # Empty entity ID + with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', return_value=4242), mock.patch.object( + self.project_config, 'logger' + ) as mock_config_logging: + self.assertIsNone( + self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + 'test_user', + 'test_user', + ) + ) + + mock_config_logging.debug.assert_called_once_with('Assigned bucket 4242 to user with bucketing ID "test_user".') + mock_config_logging.info.assert_called_once_with('User "test_user" is in no variation.') + + # Variation 2 + with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', return_value=5042), mock.patch.object( + self.project_config, 'logger' + ) as mock_config_logging: + self.assertEqual( + entities.Variation('111129', 'variation'), + self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + 'test_user', + 'test_user', + ), + ) + + mock_config_logging.debug.assert_called_once_with('Assigned bucket 5042 to user with bucketing ID "test_user".') + mock_config_logging.info.assert_called_once_with( + 'User "test_user" is in variation "variation" of experiment test_experiment.' + ) + + # No matching variation + with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', return_value=424242), mock.patch.object( + self.project_config, 'logger' + ) as mock_config_logging: + self.assertIsNone( + self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + 'test_user', + 'test_user', + ) + ) + + mock_config_logging.debug.assert_called_once_with( + 'Assigned bucket 424242 to user with bucketing ID "test_user".' + ) + mock_config_logging.info.assert_called_once_with('User "test_user" is in no variation.') + + def test_bucket__experiment_in_group(self): + """ Test that for provided bucket values correct variation ID is returned. """ + + # In group, matching experiment and variation + with mock.patch( + 'optimizely.bucketer.Bucketer._generate_bucket_value', side_effect=[42, 4242], + ), mock.patch.object(self.project_config, 'logger') as mock_config_logging: + self.assertEqual( + entities.Variation('28902', 'group_exp_1_variation'), + self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('group_exp_1'), + 'test_user', + 'test_user', + ), + ) + mock_config_logging.debug.assert_has_calls( + [ + mock.call('Assigned bucket 42 to user with bucketing ID "test_user".'), + mock.call('Assigned bucket 4242 to user with bucketing ID "test_user".'), + ] + ) + mock_config_logging.info.assert_has_calls( + [ + mock.call('User "test_user" is in experiment group_exp_1 of group 19228.'), + mock.call('User "test_user" is in variation "group_exp_1_variation" of experiment group_exp_1.'), + ] + ) + + # In group, but in no experiment + with mock.patch( + 'optimizely.bucketer.Bucketer._generate_bucket_value', side_effect=[8400, 9500], + ), mock.patch.object(self.project_config, 'logger') as mock_config_logging: + self.assertIsNone( + self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('group_exp_1'), + 'test_user', + 'test_user', + ) + ) + mock_config_logging.debug.assert_called_once_with('Assigned bucket 8400 to user with bucketing ID "test_user".') + mock_config_logging.info.assert_called_once_with('User "test_user" is in no experiment.') + + # In group, no matching experiment + with mock.patch( + 'optimizely.bucketer.Bucketer._generate_bucket_value', side_effect=[42, 9500], + ), mock.patch.object(self.project_config, 'logger') as mock_config_logging: + self.assertIsNone( + self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('group_exp_1'), + 'test_user', + 'test_user', + ) + ) + mock_config_logging.debug.assert_has_calls( + [ + mock.call('Assigned bucket 42 to user with bucketing ID "test_user".'), + mock.call('Assigned bucket 9500 to user with bucketing ID "test_user".'), + ] + ) + mock_config_logging.info.assert_has_calls( + [ + mock.call('User "test_user" is in experiment group_exp_1 of group 19228.'), + mock.call('User "test_user" is in no variation.'), + ] + ) + + # In group, experiment does not match + with mock.patch( + 'optimizely.bucketer.Bucketer._generate_bucket_value', side_effect=[42, 4242], + ), mock.patch.object(self.project_config, 'logger') as mock_config_logging: + self.assertIsNone( + self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('group_exp_2'), + 'test_user', + 'test_user', + ) + ) + mock_config_logging.debug.assert_called_once_with('Assigned bucket 42 to user with bucketing ID "test_user".') + mock_config_logging.info.assert_called_once_with( + 'User "test_user" is not in experiment "group_exp_2" of group 19228.' + ) + + # In group no matching variation + with mock.patch( + 'optimizely.bucketer.Bucketer._generate_bucket_value', side_effect=[42, 424242], + ), mock.patch.object(self.project_config, 'logger') as mock_config_logging: + self.assertIsNone( + self.bucketer.bucket( + self.project_config, + self.project_config.get_experiment_from_key('group_exp_1'), + 'test_user', + 'test_user', + ) + ) + + mock_config_logging.debug.assert_has_calls( + [ + mock.call('Assigned bucket 42 to user with bucketing ID "test_user".'), + mock.call('Assigned bucket 424242 to user with bucketing ID "test_user".'), + ] ) - ) - - mock_config_logging.debug.assert_called_once_with('Assigned bucket 42 to user with bucketing ID "test_user".') - mock_config_logging.info.assert_called_once_with( - 'User "test_user" is in variation "control" of experiment test_experiment.' - ) - - # Empty entity ID - with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', return_value=4242), \ - mock.patch.object(self.project_config, 'logger') as mock_config_logging: - self.assertIsNone(self.bucketer.bucket( - self.project_config, self.project_config.get_experiment_from_key('test_experiment'), 'test_user', 'test_user' - )) - - mock_config_logging.debug.assert_called_once_with('Assigned bucket 4242 to user with bucketing ID "test_user".') - mock_config_logging.info.assert_called_once_with('User "test_user" is in no variation.') - - # Variation 2 - with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', return_value=5042),\ - mock.patch.object(self.project_config, 'logger') as mock_config_logging: - self.assertEqual(entities.Variation('111129', 'variation'), - self.bucketer.bucket(self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), - 'test_user', - 'test_user')) - - mock_config_logging.debug.assert_called_once_with('Assigned bucket 5042 to user with bucketing ID "test_user".') - mock_config_logging.info.assert_called_once_with( - 'User "test_user" is in variation "variation" of experiment test_experiment.' - ) - - # No matching variation - with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', return_value=424242),\ - mock.patch.object(self.project_config, 'logger') as mock_config_logging: - self.assertIsNone(self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), - 'test_user', - 'test_user') - ) - - mock_config_logging.debug.assert_called_once_with('Assigned bucket 424242 to user with bucketing ID "test_user".') - mock_config_logging.info.assert_called_once_with('User "test_user" is in no variation.') - - def test_bucket__experiment_in_group(self): - """ Test that for provided bucket values correct variation ID is returned. """ - - # In group, matching experiment and variation - with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', - side_effect=[42, 4242]),\ - mock.patch.object(self.project_config, 'logger') as mock_config_logging: - self.assertEqual( - entities.Variation('28902', 'group_exp_1_variation'), - self.bucketer.bucket( - self.project_config, - self.project_config.get_experiment_from_key('group_exp_1'), - 'test_user', - 'test_user' + mock_config_logging.info.assert_has_calls( + [ + mock.call('User "test_user" is in experiment group_exp_1 of group 19228.'), + mock.call('User "test_user" is in no variation.'), + ] ) - ) - mock_config_logging.debug.assert_has_calls([ - mock.call('Assigned bucket 42 to user with bucketing ID "test_user".'), - mock.call('Assigned bucket 4242 to user with bucketing ID "test_user".') - ]) - mock_config_logging.info.assert_has_calls([ - mock.call('User "test_user" is in experiment group_exp_1 of group 19228.'), - mock.call('User "test_user" is in variation "group_exp_1_variation" of experiment group_exp_1.') - ]) - - # In group, but in no experiment - with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', - side_effect=[8400, 9500]),\ - mock.patch.object(self.project_config, 'logger') as mock_config_logging: - self.assertIsNone(self.bucketer.bucket(self.project_config, - self.project_config.get_experiment_from_key('group_exp_1'), - 'test_user', - 'test_user')) - mock_config_logging.debug.assert_called_once_with('Assigned bucket 8400 to user with bucketing ID "test_user".') - mock_config_logging.info.assert_called_once_with('User "test_user" is in no experiment.') - - # In group, no matching experiment - with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', - side_effect=[42, 9500]),\ - mock.patch.object(self.project_config, 'logger') as mock_config_logging: - self.assertIsNone(self.bucketer.bucket( - self.project_config, self.project_config.get_experiment_from_key('group_exp_1'), 'test_user', 'test_user') - ) - mock_config_logging.debug.assert_has_calls([ - mock.call('Assigned bucket 42 to user with bucketing ID "test_user".'), - mock.call('Assigned bucket 9500 to user with bucketing ID "test_user".') - ]) - mock_config_logging.info.assert_has_calls([ - mock.call('User "test_user" is in experiment group_exp_1 of group 19228.'), - mock.call('User "test_user" is in no variation.') - ]) - - # In group, experiment does not match - with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', - side_effect=[42, 4242]),\ - mock.patch.object(self.project_config, 'logger') as mock_config_logging: - self.assertIsNone(self.bucketer.bucket(self.project_config, - self.project_config.get_experiment_from_key('group_exp_2'), - 'test_user', - 'test_user')) - mock_config_logging.debug.assert_called_once_with('Assigned bucket 42 to user with bucketing ID "test_user".') - mock_config_logging.info.assert_called_once_with( - 'User "test_user" is not in experiment "group_exp_2" of group 19228.' - ) - - # In group no matching variation - with mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', - side_effect=[42, 424242]),\ - mock.patch.object(self.project_config, 'logger') as mock_config_logging: - self.assertIsNone(self.bucketer.bucket(self.project_config, - self.project_config.get_experiment_from_key('group_exp_1'), - 'test_user', - 'test_user')) - - mock_config_logging.debug.assert_has_calls([ - mock.call('Assigned bucket 42 to user with bucketing ID "test_user".'), - mock.call('Assigned bucket 424242 to user with bucketing ID "test_user".') - ]) - mock_config_logging.info.assert_has_calls([ - mock.call('User "test_user" is in experiment group_exp_1 of group 19228.'), - mock.call('User "test_user" is in no variation.') - ]) diff --git a/tests/test_config.py b/tests/test_config.py index 305cf88a..b9ca4ee9 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -25,1223 +25,1158 @@ class ConfigTest(base.BaseTest): - - def test_init(self): - """ Test that on creating object, properties are initiated correctly. """ - - self.assertEqual(self.config_dict['accountId'], self.project_config.account_id) - self.assertEqual(self.config_dict['projectId'], self.project_config.project_id) - self.assertEqual(self.config_dict['revision'], self.project_config.revision) - self.assertEqual(self.config_dict['experiments'], self.project_config.experiments) - self.assertEqual(self.config_dict['events'], self.project_config.events) - expected_group_id_map = { - '19228': entities.Group( - self.config_dict['groups'][0]['id'], - self.config_dict['groups'][0]['policy'], - self.config_dict['groups'][0]['experiments'], - self.config_dict['groups'][0]['trafficAllocation'] - ) - } - expected_experiment_key_map = { - 'test_experiment': entities.Experiment( - '111127', 'test_experiment', 'Running', ['11154'], [{ - 'key': 'control', - 'id': '111128' - }, { - 'key': 'variation', - 'id': '111129' - }], { - 'user_1': 'control', - 'user_2': 'control' - }, [{ - 'entityId': '111128', - 'endOfRange': 4000 - }, { - 'entityId': '', - 'endOfRange': 5000 - }, { - 'entityId': '111129', - 'endOfRange': 9000 - }], '111182'), - 'group_exp_1': entities.Experiment( - '32222', 'group_exp_1', 'Running', [], [{ - 'key': 'group_exp_1_control', - 'id': '28901' - }, { - 'key': 'group_exp_1_variation', - 'id': '28902' - }], { - 'user_1': 'group_exp_1_control', - 'user_2': 'group_exp_1_control' - }, [{ - 'entityId': '28901', - 'endOfRange': 3000 - }, { - 'entityId': '28902', - 'endOfRange': 9000 - }], '111183', groupId='19228', groupPolicy='random' - ), - 'group_exp_2': entities.Experiment( - '32223', 'group_exp_2', 'Running', [], [{ - 'key': 'group_exp_2_control', - 'id': '28905' - }, { - 'key': 'group_exp_2_variation', - 'id': '28906' - }], { - 'user_1': 'group_exp_2_control', - 'user_2': 'group_exp_2_control' - }, [{ - 'entityId': '28905', - 'endOfRange': 8000 - }, { - 'entityId': '28906', - 'endOfRange': 10000 - }], '111184', groupId='19228', groupPolicy='random' - ), - } - expected_experiment_id_map = { - '111127': expected_experiment_key_map.get('test_experiment'), - '32222': expected_experiment_key_map.get('group_exp_1'), - '32223': expected_experiment_key_map.get('group_exp_2') - } - expected_event_key_map = { - 'test_event': entities.Event('111095', 'test_event', ['111127']), - 'Total Revenue': entities.Event('111096', 'Total Revenue', ['111127']) - } - expected_attribute_key_map = { - 'boolean_key': entities.Attribute('111196', 'boolean_key'), - 'double_key': entities.Attribute('111198', 'double_key'), - 'integer_key': entities.Attribute('111197', 'integer_key'), - 'test_attribute': entities.Attribute('111094', 'test_attribute', segmentId='11133') - } - expected_audience_id_map = { - '11154': entities.Audience( - '11154', 'Test attribute users 1', - '["and", ["or", ["or", {"name": "test_attribute", "type": "custom_attribute", "value": "test_value_1"}]]]', - conditionStructure=['and', ['or', ['or', 0]]], - conditionList=[['test_attribute', 'test_value_1', 'custom_attribute', None]] - ), - '11159': entities.Audience( - '11159', 'Test attribute users 2', - '["and", ["or", ["or", {"name": "test_attribute", "type": "custom_attribute", "value": "test_value_2"}]]]', - conditionStructure=['and', ['or', ['or', 0]]], - conditionList=[['test_attribute', 'test_value_2', 'custom_attribute', None]] - ) - } - expected_variation_key_map = { - 'test_experiment': { - 'control': entities.Variation('111128', 'control'), - 'variation': entities.Variation('111129', 'variation') - }, - 'group_exp_1': { - 'group_exp_1_control': entities.Variation('28901', 'group_exp_1_control'), - 'group_exp_1_variation': entities.Variation('28902', 'group_exp_1_variation') - }, - 'group_exp_2': { - 'group_exp_2_control': entities.Variation('28905', 'group_exp_2_control'), - 'group_exp_2_variation': entities.Variation('28906', 'group_exp_2_variation') - } - } - expected_variation_id_map = { - 'test_experiment': { - '111128': entities.Variation('111128', 'control'), - '111129': entities.Variation('111129', 'variation') - }, - 'group_exp_1': { - '28901': entities.Variation('28901', 'group_exp_1_control'), - '28902': entities.Variation('28902', 'group_exp_1_variation') - }, - 'group_exp_2': { - '28905': entities.Variation('28905', 'group_exp_2_control'), - '28906': entities.Variation('28906', 'group_exp_2_variation') - } - } - - self.assertEqual(expected_group_id_map, self.project_config.group_id_map) - self.assertEqual(expected_experiment_key_map, self.project_config.experiment_key_map) - self.assertEqual(expected_experiment_id_map, self.project_config.experiment_id_map) - self.assertEqual(expected_event_key_map, self.project_config.event_key_map) - self.assertEqual(expected_attribute_key_map, self.project_config.attribute_key_map) - self.assertEqual(expected_audience_id_map, self.project_config.audience_id_map) - self.assertEqual(expected_variation_key_map, self.project_config.variation_key_map) - self.assertEqual(expected_variation_id_map, self.project_config.variation_id_map) - - def test_init__with_v4_datafile(self): - """ Test that on creating object, properties are initiated correctly for version 4 datafile. """ - - # Adding some additional fields like live variables and IP anonymization - config_dict = { - 'revision': '42', - 'version': '4', - 'anonymizeIP': False, - 'botFiltering': True, - 'events': [{ - 'key': 'test_event', - 'experimentIds': ['111127'], - 'id': '111095' - }, { - 'key': 'Total Revenue', - 'experimentIds': ['111127'], - 'id': '111096' - }], - 'experiments': [{ - 'key': 'test_experiment', - 'status': 'Running', - 'forcedVariations': { - 'user_1': 'control', - 'user_2': 'control' - }, - 'layerId': '111182', - 'audienceIds': ['11154'], - 'trafficAllocation': [{ - 'entityId': '111128', - 'endOfRange': 4000 - }, { - 'entityId': '', - 'endOfRange': 5000 - }, { - 'entityId': '111129', - 'endOfRange': 9000 - }], - 'id': '111127', - 'variations': [{ - 'key': 'control', - 'id': '111128', - 'variables': [{ - 'id': '127', - 'value': 'false' - }] - }, { - 'key': 'variation', - 'id': '111129', - 'variables': [{ - 'id': '127', - 'value': 'true' - }] - }] - }], - 'groups': [{ - 'id': '19228', - 'policy': 'random', - 'experiments': [{ - 'id': '32222', - 'key': 'group_exp_1', - 'status': 'Running', - 'audienceIds': [], - 'layerId': '111183', - 'variations': [{ - 'key': 'group_exp_1_control', - 'id': '28901', - 'variables': [{ - 'id': '128', - 'value': 'prod' - }, { - 'id': '129', - 'value': '1772' - }, { - 'id': '130', - 'value': '1.22992' - }] - }, { + def test_init(self): + """ Test that on creating object, properties are initiated correctly. """ + + self.assertEqual(self.config_dict['accountId'], self.project_config.account_id) + self.assertEqual(self.config_dict['projectId'], self.project_config.project_id) + self.assertEqual(self.config_dict['revision'], self.project_config.revision) + self.assertEqual(self.config_dict['experiments'], self.project_config.experiments) + self.assertEqual(self.config_dict['events'], self.project_config.events) + expected_group_id_map = { + '19228': entities.Group( + self.config_dict['groups'][0]['id'], + self.config_dict['groups'][0]['policy'], + self.config_dict['groups'][0]['experiments'], + self.config_dict['groups'][0]['trafficAllocation'], + ) + } + expected_experiment_key_map = { + 'test_experiment': entities.Experiment( + '111127', + 'test_experiment', + 'Running', + ['11154'], + [{'key': 'control', 'id': '111128'}, {'key': 'variation', 'id': '111129'}], + {'user_1': 'control', 'user_2': 'control'}, + [ + {'entityId': '111128', 'endOfRange': 4000}, + {'entityId': '', 'endOfRange': 5000}, + {'entityId': '111129', 'endOfRange': 9000}, + ], + '111182', + ), + 'group_exp_1': entities.Experiment( + '32222', + 'group_exp_1', + 'Running', + [], + [{'key': 'group_exp_1_control', 'id': '28901'}, {'key': 'group_exp_1_variation', 'id': '28902'}], + {'user_1': 'group_exp_1_control', 'user_2': 'group_exp_1_control'}, + [{'entityId': '28901', 'endOfRange': 3000}, {'entityId': '28902', 'endOfRange': 9000}], + '111183', + groupId='19228', + groupPolicy='random', + ), + 'group_exp_2': entities.Experiment( + '32223', + 'group_exp_2', + 'Running', + [], + [{'key': 'group_exp_2_control', 'id': '28905'}, {'key': 'group_exp_2_variation', 'id': '28906'}], + {'user_1': 'group_exp_2_control', 'user_2': 'group_exp_2_control'}, + [{'entityId': '28905', 'endOfRange': 8000}, {'entityId': '28906', 'endOfRange': 10000}], + '111184', + groupId='19228', + groupPolicy='random', + ), + } + expected_experiment_id_map = { + '111127': expected_experiment_key_map.get('test_experiment'), + '32222': expected_experiment_key_map.get('group_exp_1'), + '32223': expected_experiment_key_map.get('group_exp_2'), + } + expected_event_key_map = { + 'test_event': entities.Event('111095', 'test_event', ['111127']), + 'Total Revenue': entities.Event('111096', 'Total Revenue', ['111127']), + } + expected_attribute_key_map = { + 'boolean_key': entities.Attribute('111196', 'boolean_key'), + 'double_key': entities.Attribute('111198', 'double_key'), + 'integer_key': entities.Attribute('111197', 'integer_key'), + 'test_attribute': entities.Attribute('111094', 'test_attribute', segmentId='11133'), + } + expected_audience_id_map = { + '11154': entities.Audience( + '11154', + 'Test attribute users 1', + '["and", ["or", ["or", {"name": "test_attribute", ' + '"type": "custom_attribute", "value": "test_value_1"}]]]', + conditionStructure=['and', ['or', ['or', 0]]], + conditionList=[['test_attribute', 'test_value_1', 'custom_attribute', None]], + ), + '11159': entities.Audience( + '11159', + 'Test attribute users 2', + '["and", ["or", ["or", {"name": "test_attribute", ' + '"type": "custom_attribute", "value": "test_value_2"}]]]', + conditionStructure=['and', ['or', ['or', 0]]], + conditionList=[['test_attribute', 'test_value_2', 'custom_attribute', None]], + ), + } + expected_variation_key_map = { + 'test_experiment': { + 'control': entities.Variation('111128', 'control'), + 'variation': entities.Variation('111129', 'variation'), + }, + 'group_exp_1': { + 'group_exp_1_control': entities.Variation('28901', 'group_exp_1_control'), + 'group_exp_1_variation': entities.Variation('28902', 'group_exp_1_variation'), + }, + 'group_exp_2': { + 'group_exp_2_control': entities.Variation('28905', 'group_exp_2_control'), + 'group_exp_2_variation': entities.Variation('28906', 'group_exp_2_variation'), + }, + } + expected_variation_id_map = { + 'test_experiment': { + '111128': entities.Variation('111128', 'control'), + '111129': entities.Variation('111129', 'variation'), + }, + 'group_exp_1': { + '28901': entities.Variation('28901', 'group_exp_1_control'), + '28902': entities.Variation('28902', 'group_exp_1_variation'), + }, + 'group_exp_2': { + '28905': entities.Variation('28905', 'group_exp_2_control'), + '28906': entities.Variation('28906', 'group_exp_2_variation'), + }, + } + + self.assertEqual(expected_group_id_map, self.project_config.group_id_map) + self.assertEqual(expected_experiment_key_map, self.project_config.experiment_key_map) + self.assertEqual(expected_experiment_id_map, self.project_config.experiment_id_map) + self.assertEqual(expected_event_key_map, self.project_config.event_key_map) + self.assertEqual(expected_attribute_key_map, self.project_config.attribute_key_map) + self.assertEqual(expected_audience_id_map, self.project_config.audience_id_map) + self.assertEqual(expected_variation_key_map, self.project_config.variation_key_map) + self.assertEqual(expected_variation_id_map, self.project_config.variation_id_map) + + def test_init__with_v4_datafile(self): + """ Test that on creating object, properties are initiated correctly for version 4 datafile. """ + + # Adding some additional fields like live variables and IP anonymization + config_dict = { + 'revision': '42', + 'version': '4', + 'anonymizeIP': False, + 'botFiltering': True, + 'events': [ + {'key': 'test_event', 'experimentIds': ['111127'], 'id': '111095'}, + {'key': 'Total Revenue', 'experimentIds': ['111127'], 'id': '111096'}, + ], + 'experiments': [ + { + 'key': 'test_experiment', + 'status': 'Running', + 'forcedVariations': {'user_1': 'control', 'user_2': 'control'}, + 'layerId': '111182', + 'audienceIds': ['11154'], + 'trafficAllocation': [ + {'entityId': '111128', 'endOfRange': 4000}, + {'entityId': '', 'endOfRange': 5000}, + {'entityId': '111129', 'endOfRange': 9000}, + ], + 'id': '111127', + 'variations': [ + {'key': 'control', 'id': '111128', 'variables': [{'id': '127', 'value': 'false'}]}, + {'key': 'variation', 'id': '111129', 'variables': [{'id': '127', 'value': 'true'}]}, + ], + } + ], + 'groups': [ + { + 'id': '19228', + 'policy': 'random', + 'experiments': [ + { + 'id': '32222', + 'key': 'group_exp_1', + 'status': 'Running', + 'audienceIds': [], + 'layerId': '111183', + 'variations': [ + { + 'key': 'group_exp_1_control', + 'id': '28901', + 'variables': [ + {'id': '128', 'value': 'prod'}, + {'id': '129', 'value': '1772'}, + {'id': '130', 'value': '1.22992'}, + ], + }, + { + 'key': 'group_exp_1_variation', + 'id': '28902', + 'variables': [ + {'id': '128', 'value': 'stage'}, + {'id': '129', 'value': '112'}, + {'id': '130', 'value': '1.211'}, + ], + }, + ], + 'forcedVariations': {'user_1': 'group_exp_1_control', 'user_2': 'group_exp_1_control'}, + 'trafficAllocation': [ + {'entityId': '28901', 'endOfRange': 3000}, + {'entityId': '28902', 'endOfRange': 9000}, + ], + }, + { + 'id': '32223', + 'key': 'group_exp_2', + 'status': 'Running', + 'audienceIds': [], + 'layerId': '111184', + 'variations': [ + {'key': 'group_exp_2_control', 'id': '28905', 'variables': []}, + {'key': 'group_exp_2_variation', 'id': '28906', 'variables': []}, + ], + 'forcedVariations': {'user_1': 'group_exp_2_control', 'user_2': 'group_exp_2_control'}, + 'trafficAllocation': [ + {'entityId': '28905', 'endOfRange': 8000}, + {'entityId': '28906', 'endOfRange': 10000}, + ], + }, + ], + 'trafficAllocation': [ + {'entityId': '32222', 'endOfRange': 3000}, + {'entityId': '32223', 'endOfRange': 7500}, + ], + } + ], + 'accountId': '12001', + 'attributes': [{'key': 'test_attribute', 'id': '111094'}], + 'audiences': [ + { + 'name': 'Test attribute users', + 'conditions': '["and", ["or", ["or", ' + '{"name": "test_attribute", "type": "custom_attribute", "value": "test_value"}]]]', + 'id': '11154', + } + ], + 'rollouts': [ + { + 'id': '211111', + 'experiments': [ + { + 'key': '211112', + 'status': 'Running', + 'forcedVariations': {}, + 'layerId': '211111', + 'audienceIds': ['11154'], + 'trafficAllocation': [{'entityId': '211113', 'endOfRange': 10000}], + 'id': '211112', + 'variations': [ + {'id': '211113', 'key': '211113', 'variables': [{'id': '131', 'value': '15'}]} + ], + } + ], + } + ], + 'featureFlags': [ + { + 'id': '91111', + 'key': 'test_feature_in_experiment', + 'experimentIds': ['111127'], + 'rolloutId': '', + 'variables': [ + {'id': '127', 'key': 'is_working', 'defaultValue': 'true', 'type': 'boolean'}, + {'id': '128', 'key': 'environment', 'defaultValue': 'devel', 'type': 'string'}, + {'id': '129', 'key': 'number_of_days', 'defaultValue': '192', 'type': 'integer'}, + {'id': '130', 'key': 'significance_value', 'defaultValue': '0.00098', 'type': 'double'}, + ], + }, + { + 'id': '91112', + 'key': 'test_feature_in_rollout', + 'rolloutId': '211111', + 'experimentIds': [], + 'variables': [{'id': '131', 'key': 'number_of_projects', 'defaultValue': '10', 'type': 'integer'}], + }, + { + 'id': '91113', + 'key': 'test_feature_in_group', + 'rolloutId': '', + 'experimentIds': ['32222'], + 'variables': [], + }, + ], + 'projectId': '111001', + } + + test_obj = optimizely.Optimizely(json.dumps(config_dict)) + project_config = test_obj.config_manager.get_config() + self.assertEqual(config_dict['accountId'], project_config.account_id) + self.assertEqual(config_dict['projectId'], project_config.project_id) + self.assertEqual(config_dict['revision'], project_config.revision) + self.assertEqual(config_dict['experiments'], project_config.experiments) + self.assertEqual(config_dict['events'], project_config.events) + self.assertEqual(config_dict['botFiltering'], project_config.bot_filtering) + + expected_group_id_map = { + '19228': entities.Group( + config_dict['groups'][0]['id'], + config_dict['groups'][0]['policy'], + config_dict['groups'][0]['experiments'], + config_dict['groups'][0]['trafficAllocation'], + ) + } + expected_experiment_key_map = { + 'test_experiment': entities.Experiment( + '111127', + 'test_experiment', + 'Running', + ['11154'], + [ + {'key': 'control', 'id': '111128', 'variables': [{'id': '127', 'value': 'false'}]}, + {'key': 'variation', 'id': '111129', 'variables': [{'id': '127', 'value': 'true'}]}, + ], + {'user_1': 'control', 'user_2': 'control'}, + [ + {'entityId': '111128', 'endOfRange': 4000}, + {'entityId': '', 'endOfRange': 5000}, + {'entityId': '111129', 'endOfRange': 9000}, + ], + '111182', + ), + 'group_exp_1': entities.Experiment( + '32222', + 'group_exp_1', + 'Running', + [], + [ + { + 'key': 'group_exp_1_control', + 'id': '28901', + 'variables': [ + {'id': '128', 'value': 'prod'}, + {'id': '129', 'value': '1772'}, + {'id': '130', 'value': '1.22992'}, + ], + }, + { + 'key': 'group_exp_1_variation', + 'id': '28902', + 'variables': [ + {'id': '128', 'value': 'stage'}, + {'id': '129', 'value': '112'}, + {'id': '130', 'value': '1.211'}, + ], + }, + ], + {'user_1': 'group_exp_1_control', 'user_2': 'group_exp_1_control'}, + [{'entityId': '28901', 'endOfRange': 3000}, {'entityId': '28902', 'endOfRange': 9000}], + '111183', + groupId='19228', + groupPolicy='random', + ), + 'group_exp_2': entities.Experiment( + '32223', + 'group_exp_2', + 'Running', + [], + [ + {'key': 'group_exp_2_control', 'id': '28905', 'variables': []}, + {'key': 'group_exp_2_variation', 'id': '28906', 'variables': []}, + ], + {'user_1': 'group_exp_2_control', 'user_2': 'group_exp_2_control'}, + [{'entityId': '28905', 'endOfRange': 8000}, {'entityId': '28906', 'endOfRange': 10000}], + '111184', + groupId='19228', + groupPolicy='random', + ), + '211112': entities.Experiment( + '211112', + '211112', + 'Running', + ['11154'], + [{'id': '211113', 'key': '211113', 'variables': [{'id': '131', 'value': '15'}]}], + {}, + [{'entityId': '211113', 'endOfRange': 10000}], + '211111', + ), + } + expected_experiment_id_map = { + '111127': expected_experiment_key_map.get('test_experiment'), + '32222': expected_experiment_key_map.get('group_exp_1'), + '32223': expected_experiment_key_map.get('group_exp_2'), + '211112': expected_experiment_key_map.get('211112'), + } + expected_event_key_map = { + 'test_event': entities.Event('111095', 'test_event', ['111127']), + 'Total Revenue': entities.Event('111096', 'Total Revenue', ['111127']), + } + expected_attribute_key_map = { + 'test_attribute': entities.Attribute('111094', 'test_attribute', segmentId='11133') + } + expected_audience_id_map = { + '11154': entities.Audience( + '11154', + 'Test attribute users', + '["and", ["or", ["or", {"name": "test_attribute", ' + '"type": "custom_attribute", "value": "test_value"}]]]', + conditionStructure=['and', ['or', ['or', 0]]], + conditionList=[['test_attribute', 'test_value', 'custom_attribute', None]], + ) + } + expected_variation_key_map = { + 'test_experiment': { + 'control': entities.Variation('111128', 'control', False, [{'id': '127', 'value': 'false'}]), + 'variation': entities.Variation('111129', 'variation', False, [{'id': '127', 'value': 'true'}]), + }, + 'group_exp_1': { + 'group_exp_1_control': entities.Variation( + '28901', + 'group_exp_1_control', + False, + [ + {'id': '128', 'value': 'prod'}, + {'id': '129', 'value': '1772'}, + {'id': '130', 'value': '1.22992'}, + ], + ), + 'group_exp_1_variation': entities.Variation( + '28902', + 'group_exp_1_variation', + False, + [{'id': '128', 'value': 'stage'}, {'id': '129', 'value': '112'}, {'id': '130', 'value': '1.211'}], + ), + }, + 'group_exp_2': { + 'group_exp_2_control': entities.Variation('28905', 'group_exp_2_control'), + 'group_exp_2_variation': entities.Variation('28906', 'group_exp_2_variation'), + }, + '211112': {'211113': entities.Variation('211113', '211113', False, [{'id': '131', 'value': '15'}])}, + } + expected_variation_id_map = { + 'test_experiment': { + '111128': entities.Variation('111128', 'control', False, [{'id': '127', 'value': 'false'}]), + '111129': entities.Variation('111129', 'variation', False, [{'id': '127', 'value': 'true'}]), + }, + 'group_exp_1': { + '28901': entities.Variation( + '28901', + 'group_exp_1_control', + False, + [ + {'id': '128', 'value': 'prod'}, + {'id': '129', 'value': '1772'}, + {'id': '130', 'value': '1.22992'}, + ], + ), + '28902': entities.Variation( + '28902', + 'group_exp_1_variation', + False, + [{'id': '128', 'value': 'stage'}, {'id': '129', 'value': '112'}, {'id': '130', 'value': '1.211'}], + ), + }, + 'group_exp_2': { + '28905': entities.Variation('28905', 'group_exp_2_control'), + '28906': entities.Variation('28906', 'group_exp_2_variation'), + }, + '211112': {'211113': entities.Variation('211113', '211113', False, [{'id': '131', 'value': '15'}])}, + } + + expected_feature_key_map = { + 'test_feature_in_experiment': entities.FeatureFlag( + '91111', + 'test_feature_in_experiment', + ['111127'], + '', + { + 'is_working': entities.Variable('127', 'is_working', 'boolean', 'true'), + 'environment': entities.Variable('128', 'environment', 'string', 'devel'), + 'number_of_days': entities.Variable('129', 'number_of_days', 'integer', '192'), + 'significance_value': entities.Variable('130', 'significance_value', 'double', '0.00098'), + }, + ), + 'test_feature_in_rollout': entities.FeatureFlag( + '91112', + 'test_feature_in_rollout', + [], + '211111', + {'number_of_projects': entities.Variable('131', 'number_of_projects', 'integer', '10')}, + ), + 'test_feature_in_group': entities.FeatureFlag('91113', 'test_feature_in_group', ['32222'], '', {}, '19228'), + } + + expected_rollout_id_map = { + '211111': entities.Layer( + '211111', + [ + { + 'key': '211112', + 'status': 'Running', + 'forcedVariations': {}, + 'layerId': '211111', + 'audienceIds': ['11154'], + 'trafficAllocation': [{'entityId': '211113', 'endOfRange': 10000}], + 'id': '211112', + 'variations': [{'id': '211113', 'key': '211113', 'variables': [{'id': '131', 'value': '15'}]}], + } + ], + ) + } + + expected_variation_variable_usage_map = { + '111128': {'127': entities.Variation.VariableUsage('127', 'false')}, + '111129': {'127': entities.Variation.VariableUsage('127', 'true')}, + '28901': { + '128': entities.Variation.VariableUsage('128', 'prod'), + '129': entities.Variation.VariableUsage('129', '1772'), + '130': entities.Variation.VariableUsage('130', '1.22992'), + }, + '28902': { + '128': entities.Variation.VariableUsage('128', 'stage'), + '129': entities.Variation.VariableUsage('129', '112'), + '130': entities.Variation.VariableUsage('130', '1.211'), + }, + '28905': {}, + '28906': {}, + '211113': {'131': entities.Variation.VariableUsage('131', '15')}, + } + + expected_experiment_feature_map = {'111127': ['91111'], '32222': ['91113']} + + self.assertEqual( + expected_variation_variable_usage_map['28901'], project_config.variation_variable_usage_map['28901'], + ) + self.assertEqual(expected_group_id_map, project_config.group_id_map) + self.assertEqual(expected_experiment_key_map, project_config.experiment_key_map) + self.assertEqual(expected_experiment_id_map, project_config.experiment_id_map) + self.assertEqual(expected_event_key_map, project_config.event_key_map) + self.assertEqual(expected_attribute_key_map, project_config.attribute_key_map) + self.assertEqual(expected_audience_id_map, project_config.audience_id_map) + self.assertEqual(expected_variation_key_map, project_config.variation_key_map) + self.assertEqual(expected_variation_id_map, project_config.variation_id_map) + self.assertEqual(expected_feature_key_map, project_config.feature_key_map) + self.assertEqual(expected_rollout_id_map, project_config.rollout_id_map) + self.assertEqual( + expected_variation_variable_usage_map, project_config.variation_variable_usage_map, + ) + self.assertEqual(expected_experiment_feature_map, project_config.experiment_feature_map) + + def test_variation_has_featureEnabled_false_if_prop_undefined(self): + """ Test that featureEnabled property by default is set to False, when not given in the data file""" + variation = { 'key': 'group_exp_1_variation', 'id': '28902', - 'variables': [{ - 'id': '128', - 'value': 'stage' - }, { - 'id': '129', - 'value': '112' - }, { - 'id': '130', - 'value': '1.211' - }] - }], - 'forcedVariations': { - 'user_1': 'group_exp_1_control', - 'user_2': 'group_exp_1_control' - }, - 'trafficAllocation': [{ - 'entityId': '28901', - 'endOfRange': 3000 - }, { - 'entityId': '28902', - 'endOfRange': 9000 - }] - }, { - 'id': '32223', - 'key': 'group_exp_2', - 'status': 'Running', - 'audienceIds': [], - 'layerId': '111184', - 'variations': [{ - 'key': 'group_exp_2_control', - 'id': '28905', - 'variables': [] - }, { - 'key': 'group_exp_2_variation', - 'id': '28906', - 'variables': [] - }], - 'forcedVariations': { - 'user_1': 'group_exp_2_control', - 'user_2': 'group_exp_2_control' - }, - 'trafficAllocation': [{ - 'entityId': '28905', - 'endOfRange': 8000 - }, { - 'entityId': '28906', - 'endOfRange': 10000 - }] - }], - 'trafficAllocation': [{ - 'entityId': '32222', - 'endOfRange': 3000 - }, { - 'entityId': '32223', - 'endOfRange': 7500 - }] - }], - 'accountId': '12001', - 'attributes': [{ - 'key': 'test_attribute', - 'id': '111094' - }], - 'audiences': [{ - 'name': 'Test attribute users', - 'conditions': '["and", ["or", ["or", ' - '{"name": "test_attribute", "type": "custom_attribute", "value": "test_value"}]]]', - 'id': '11154' - }], - 'rollouts': [{ - 'id': '211111', - 'experiments': [{ - 'key': '211112', - 'status': 'Running', - 'forcedVariations': {}, - 'layerId': '211111', - 'audienceIds': ['11154'], - 'trafficAllocation': [{ - 'entityId': '211113', - 'endOfRange': 10000 - }], - 'id': '211112', - 'variations': [{ - 'id': '211113', - 'key': '211113', - 'variables': [{ - 'id': '131', - 'value': '15' - }] - }] - }] - }], - 'featureFlags': [{ - 'id': '91111', - 'key': 'test_feature_in_experiment', - 'experimentIds': ['111127'], - 'rolloutId': '', - 'variables': [{ - 'id': '127', - 'key': 'is_working', - 'defaultValue': 'true', - 'type': 'boolean', - }, { - 'id': '128', - 'key': 'environment', - 'defaultValue': 'devel', - 'type': 'string', - }, { - 'id': '129', - 'key': 'number_of_days', - 'defaultValue': '192', - 'type': 'integer', - }, { - 'id': '130', - 'key': 'significance_value', - 'defaultValue': '0.00098', - 'type': 'double', - }] - }, { - 'id': '91112', - 'key': 'test_feature_in_rollout', - 'rolloutId': '211111', - 'experimentIds': [], - 'variables': [{ - 'id': '131', - 'key': 'number_of_projects', - 'defaultValue': '10', - 'type': 'integer', - }], - }, { - 'id': '91113', - 'key': 'test_feature_in_group', - 'rolloutId': '', - 'experimentIds': ['32222'], - 'variables': [], - }], - 'projectId': '111001' - } - - test_obj = optimizely.Optimizely(json.dumps(config_dict)) - project_config = test_obj.config_manager.get_config() - self.assertEqual(config_dict['accountId'], project_config.account_id) - self.assertEqual(config_dict['projectId'], project_config.project_id) - self.assertEqual(config_dict['revision'], project_config.revision) - self.assertEqual(config_dict['experiments'], project_config.experiments) - self.assertEqual(config_dict['events'], project_config.events) - self.assertEqual(config_dict['botFiltering'], project_config.bot_filtering) - - expected_group_id_map = { - '19228': entities.Group( - config_dict['groups'][0]['id'], - config_dict['groups'][0]['policy'], - config_dict['groups'][0]['experiments'], - config_dict['groups'][0]['trafficAllocation'] - ) - } - expected_experiment_key_map = { - 'test_experiment': entities.Experiment( - '111127', 'test_experiment', 'Running', ['11154'], [{ - 'key': 'control', - 'id': '111128', - 'variables': [{ - 'id': '127', - 'value': 'false' - }] - }, { - 'key': 'variation', - 'id': '111129', - 'variables': [{ - 'id': '127', - 'value': 'true' - }] - }], { - 'user_1': 'control', - 'user_2': 'control' - }, [{ - 'entityId': '111128', - 'endOfRange': 4000 - }, { - 'entityId': '', - 'endOfRange': 5000 - }, { - 'entityId': '111129', - 'endOfRange': 9000 - }], '111182'), - 'group_exp_1': entities.Experiment( - '32222', 'group_exp_1', 'Running', [], [{ - 'key': 'group_exp_1_control', - 'id': '28901', - 'variables': [{ - 'id': '128', - 'value': 'prod' - }, { - 'id': '129', - 'value': '1772' - }, { - 'id': '130', - 'value': '1.22992' - }] - }, { - 'key': 'group_exp_1_variation', - 'id': '28902', - 'variables': [{ - 'id': '128', - 'value': 'stage' - }, { - 'id': '129', - 'value': '112' - }, { - 'id': '130', - 'value': '1.211' - }] - }], { - 'user_1': 'group_exp_1_control', - 'user_2': 'group_exp_1_control' - }, [{ - 'entityId': '28901', - 'endOfRange': 3000 - }, { - 'entityId': '28902', - 'endOfRange': 9000 - }], '111183', groupId='19228', groupPolicy='random' - ), - 'group_exp_2': entities.Experiment( - '32223', 'group_exp_2', 'Running', [], [{ - 'key': 'group_exp_2_control', - 'id': '28905', - 'variables': [] - }, { - 'key': 'group_exp_2_variation', - 'id': '28906', - 'variables': [] - }], { - 'user_1': 'group_exp_2_control', - 'user_2': 'group_exp_2_control' - }, [{ - 'entityId': '28905', - 'endOfRange': 8000 - }, { - 'entityId': '28906', - 'endOfRange': 10000 - }], '111184', groupId='19228', groupPolicy='random' - ), - '211112': entities.Experiment( - '211112', '211112', 'Running', ['11154'], [{ - 'id': '211113', - 'key': '211113', - 'variables': [{ - 'id': '131', - 'value': '15', - }] - }], {}, [{ - 'entityId': '211113', - 'endOfRange': 10000 - }], - '211111' - ), - } - expected_experiment_id_map = { - '111127': expected_experiment_key_map.get('test_experiment'), - '32222': expected_experiment_key_map.get('group_exp_1'), - '32223': expected_experiment_key_map.get('group_exp_2'), - '211112': expected_experiment_key_map.get('211112') - } - expected_event_key_map = { - 'test_event': entities.Event('111095', 'test_event', ['111127']), - 'Total Revenue': entities.Event('111096', 'Total Revenue', ['111127']) - } - expected_attribute_key_map = { - 'test_attribute': entities.Attribute('111094', 'test_attribute', segmentId='11133') - } - expected_audience_id_map = { - '11154': entities.Audience( - '11154', 'Test attribute users', - '["and", ["or", ["or", {"name": "test_attribute", "type": "custom_attribute", "value": "test_value"}]]]', - conditionStructure=['and', ['or', ['or', 0]]], - conditionList=[['test_attribute', 'test_value', 'custom_attribute', None]] - ) - } - expected_variation_key_map = { - 'test_experiment': { - 'control': entities.Variation('111128', 'control', False, [{'id': '127', 'value': 'false'}]), - 'variation': entities.Variation('111129', 'variation', False, [{'id': '127', 'value': 'true'}]) - }, - 'group_exp_1': { - 'group_exp_1_control': entities.Variation( - '28901', 'group_exp_1_control', False, [ - {'id': '128', 'value': 'prod'}, {'id': '129', 'value': '1772'}, {'id': '130', 'value': '1.22992'}]), - 'group_exp_1_variation': entities.Variation( - '28902', 'group_exp_1_variation', False, [ - {'id': '128', 'value': 'stage'}, {'id': '129', 'value': '112'}, {'id': '130', 'value': '1.211'}]) - }, - 'group_exp_2': { - 'group_exp_2_control': entities.Variation('28905', 'group_exp_2_control'), - 'group_exp_2_variation': entities.Variation('28906', 'group_exp_2_variation') - }, - '211112': { - '211113': entities.Variation('211113', '211113', False, [{'id': '131', 'value': '15'}]) - } - } - expected_variation_id_map = { - 'test_experiment': { - '111128': entities.Variation('111128', 'control', False, [{'id': '127', 'value': 'false'}]), - '111129': entities.Variation('111129', 'variation', False, [{'id': '127', 'value': 'true'}]) - }, - 'group_exp_1': { - '28901': entities.Variation('28901', 'group_exp_1_control', False, [ - {'id': '128', 'value': 'prod'}, {'id': '129', 'value': '1772'}, {'id': '130', 'value': '1.22992'}]), - '28902': entities.Variation('28902', 'group_exp_1_variation', False, [ - {'id': '128', 'value': 'stage'}, {'id': '129', 'value': '112'}, {'id': '130', 'value': '1.211'}]) - }, - 'group_exp_2': { - '28905': entities.Variation('28905', 'group_exp_2_control'), - '28906': entities.Variation('28906', 'group_exp_2_variation') - }, - '211112': { - '211113': entities.Variation('211113', '211113', False, [{'id': '131', 'value': '15'}]) - } - } - - expected_feature_key_map = { - 'test_feature_in_experiment': entities.FeatureFlag('91111', 'test_feature_in_experiment', ['111127'], '', { - 'is_working': entities.Variable('127', 'is_working', 'boolean', 'true'), - 'environment': entities.Variable('128', 'environment', 'string', 'devel'), - 'number_of_days': entities.Variable('129', 'number_of_days', 'integer', '192'), - 'significance_value': entities.Variable('130', 'significance_value', 'double', '0.00098') - }), - 'test_feature_in_rollout': entities.FeatureFlag('91112', 'test_feature_in_rollout', [], '211111', { - 'number_of_projects': entities.Variable('131', 'number_of_projects', 'integer', '10') - }), - 'test_feature_in_group': entities.FeatureFlag('91113', 'test_feature_in_group', ['32222'], '', {}, '19228') - } - - expected_rollout_id_map = { - '211111': entities.Layer('211111', [{ - 'key': '211112', - 'status': 'Running', - 'forcedVariations': {}, - 'layerId': '211111', - 'audienceIds': ['11154'], - 'trafficAllocation': [{ - 'entityId': '211113', - 'endOfRange': 10000 - }], - 'id': '211112', - 'variations': [{ - 'id': '211113', - 'key': '211113', - 'variables': [{ - 'id': '131', - 'value': '15' - }] - }] - }] - ) - } - - expected_variation_variable_usage_map = { - '111128': { - '127': entities.Variation.VariableUsage('127', 'false') - }, - '111129': { - '127': entities.Variation.VariableUsage('127', 'true') - }, - '28901': { - '128': entities.Variation.VariableUsage('128', 'prod'), - '129': entities.Variation.VariableUsage('129', '1772'), - '130': entities.Variation.VariableUsage('130', '1.22992') - }, - '28902': { - '128': entities.Variation.VariableUsage('128', 'stage'), - '129': entities.Variation.VariableUsage('129', '112'), - '130': entities.Variation.VariableUsage('130', '1.211') - }, - '28905': {}, - '28906': {}, - '211113': { - '131': entities.Variation.VariableUsage('131', '15') - } - } - - expected_experiment_feature_map = { - '111127': ['91111'], - '32222': ['91113'] - } - - self.assertEqual(expected_variation_variable_usage_map['28901'], - project_config.variation_variable_usage_map['28901']) - self.assertEqual(expected_group_id_map, project_config.group_id_map) - self.assertEqual(expected_experiment_key_map, project_config.experiment_key_map) - self.assertEqual(expected_experiment_id_map, project_config.experiment_id_map) - self.assertEqual(expected_event_key_map, project_config.event_key_map) - self.assertEqual(expected_attribute_key_map, project_config.attribute_key_map) - self.assertEqual(expected_audience_id_map, project_config.audience_id_map) - self.assertEqual(expected_variation_key_map, project_config.variation_key_map) - self.assertEqual(expected_variation_id_map, project_config.variation_id_map) - self.assertEqual(expected_feature_key_map, project_config.feature_key_map) - self.assertEqual(expected_rollout_id_map, project_config.rollout_id_map) - self.assertEqual(expected_variation_variable_usage_map, project_config.variation_variable_usage_map) - self.assertEqual(expected_experiment_feature_map, project_config.experiment_feature_map) - - def test_variation_has_featureEnabled_false_if_prop_undefined(self): - """ Test that featureEnabled property by default is set to False, when not given in the data file""" - variation = { - 'key': 'group_exp_1_variation', - 'id': '28902', - 'variables': [{ - 'id': '128', - 'value': 'stage' - }, { - 'id': '129', - 'value': '112' - }, { - 'id': '130', - 'value': '1.211' - }] - } - - variation_entity = entities.Variation(**variation) - - self.assertEqual(variation['id'], variation_entity.id) - self.assertEqual(variation['key'], variation_entity.key) - self.assertEqual(variation['variables'], variation_entity.variables) - self.assertFalse(variation_entity.featureEnabled) - - def test_get_version(self): - """ Test that JSON version is retrieved correctly when using get_version. """ - - self.assertEqual('2', self.project_config.get_version()) - - def test_get_revision(self): - """ Test that revision is retrieved correctly when using get_revision. """ - - self.assertEqual('42', self.project_config.get_revision()) - - def test_get_account_id(self): - """ Test that account ID is retrieved correctly when using get_account_id. """ - - self.assertEqual(self.config_dict['accountId'], self.project_config.get_account_id()) - - def test_get_project_id(self): - """ Test that project ID is retrieved correctly when using get_project_id. """ - - self.assertEqual(self.config_dict['projectId'], self.project_config.get_project_id()) - - def test_get_bot_filtering(self): - """ Test that bot filtering is retrieved correctly when using get_bot_filtering_value. """ - - # Assert bot filtering is None when not provided in data file - self.assertTrue('botFiltering' not in self.config_dict) - self.assertIsNone(self.project_config.get_bot_filtering_value()) - - # Assert bot filtering is retrieved as provided in the data file - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - project_config = opt_obj.config_manager.get_config() - self.assertEqual( - self.config_dict_with_features['botFiltering'], - project_config.get_bot_filtering_value() - ) - - def test_get_experiment_from_key__valid_key(self): - """ Test that experiment is retrieved correctly for valid experiment key. """ - - self.assertEqual(entities.Experiment( - '32222', 'group_exp_1', 'Running', [], [{ - 'key': 'group_exp_1_control', - 'id': '28901' - }, { - 'key': 'group_exp_1_variation', - 'id': '28902' - }], { - 'user_1': 'group_exp_1_control', - 'user_2': 'group_exp_1_control' - }, [{ - 'entityId': '28901', - 'endOfRange': 3000 - }, { - 'entityId': '28902', - 'endOfRange': 9000 - }], '111183', groupId='19228', groupPolicy='random'), - self.project_config.get_experiment_from_key('group_exp_1')) - - def test_get_experiment_from_key__invalid_key(self): - """ Test that None is returned when provided experiment key is invalid. """ - - self.assertIsNone(self.project_config.get_experiment_from_key('invalid_key')) - - def test_get_experiment_from_id__valid_id(self): - """ Test that experiment is retrieved correctly for valid experiment ID. """ - - self.assertEqual(entities.Experiment( - '32222', 'group_exp_1', 'Running', [], [{ - 'key': 'group_exp_1_control', - 'id': '28901' - }, { - 'key': 'group_exp_1_variation', - 'id': '28902' - }], { - 'user_1': 'group_exp_1_control', - 'user_2': 'group_exp_1_control' - }, [{ - 'entityId': '28901', - 'endOfRange': 3000 - }, { - 'entityId': '28902', - 'endOfRange': 9000 - }], '111183', groupId='19228', groupPolicy='random'), - self.project_config.get_experiment_from_id('32222')) + 'variables': [ + {'id': '128', 'value': 'stage'}, + {'id': '129', 'value': '112'}, + {'id': '130', 'value': '1.211'}, + ], + } + + variation_entity = entities.Variation(**variation) + + self.assertEqual(variation['id'], variation_entity.id) + self.assertEqual(variation['key'], variation_entity.key) + self.assertEqual(variation['variables'], variation_entity.variables) + self.assertFalse(variation_entity.featureEnabled) + + def test_get_version(self): + """ Test that JSON version is retrieved correctly when using get_version. """ + + self.assertEqual('2', self.project_config.get_version()) + + def test_get_revision(self): + """ Test that revision is retrieved correctly when using get_revision. """ + + self.assertEqual('42', self.project_config.get_revision()) + + def test_get_account_id(self): + """ Test that account ID is retrieved correctly when using get_account_id. """ + + self.assertEqual(self.config_dict['accountId'], self.project_config.get_account_id()) + + def test_get_project_id(self): + """ Test that project ID is retrieved correctly when using get_project_id. """ + + self.assertEqual(self.config_dict['projectId'], self.project_config.get_project_id()) + + def test_get_bot_filtering(self): + """ Test that bot filtering is retrieved correctly when using get_bot_filtering_value. """ + + # Assert bot filtering is None when not provided in data file + self.assertTrue('botFiltering' not in self.config_dict) + self.assertIsNone(self.project_config.get_bot_filtering_value()) + + # Assert bot filtering is retrieved as provided in the data file + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() + self.assertEqual( + self.config_dict_with_features['botFiltering'], project_config.get_bot_filtering_value(), + ) + + def test_get_experiment_from_key__valid_key(self): + """ Test that experiment is retrieved correctly for valid experiment key. """ + + self.assertEqual( + entities.Experiment( + '32222', + 'group_exp_1', + 'Running', + [], + [{'key': 'group_exp_1_control', 'id': '28901'}, {'key': 'group_exp_1_variation', 'id': '28902'}], + {'user_1': 'group_exp_1_control', 'user_2': 'group_exp_1_control'}, + [{'entityId': '28901', 'endOfRange': 3000}, {'entityId': '28902', 'endOfRange': 9000}], + '111183', + groupId='19228', + groupPolicy='random', + ), + self.project_config.get_experiment_from_key('group_exp_1'), + ) + + def test_get_experiment_from_key__invalid_key(self): + """ Test that None is returned when provided experiment key is invalid. """ + + self.assertIsNone(self.project_config.get_experiment_from_key('invalid_key')) + + def test_get_experiment_from_id__valid_id(self): + """ Test that experiment is retrieved correctly for valid experiment ID. """ + + self.assertEqual( + entities.Experiment( + '32222', + 'group_exp_1', + 'Running', + [], + [{'key': 'group_exp_1_control', 'id': '28901'}, {'key': 'group_exp_1_variation', 'id': '28902'}], + {'user_1': 'group_exp_1_control', 'user_2': 'group_exp_1_control'}, + [{'entityId': '28901', 'endOfRange': 3000}, {'entityId': '28902', 'endOfRange': 9000}], + '111183', + groupId='19228', + groupPolicy='random', + ), + self.project_config.get_experiment_from_id('32222'), + ) - def test_get_experiment_from_id__invalid_id(self): - """ Test that None is returned when provided experiment ID is invalid. """ + def test_get_experiment_from_id__invalid_id(self): + """ Test that None is returned when provided experiment ID is invalid. """ - self.assertIsNone(self.project_config.get_experiment_from_id('invalid_id')) + self.assertIsNone(self.project_config.get_experiment_from_id('invalid_id')) - def test_get_audience__valid_id(self): - """ Test that audience object is retrieved correctly given a valid audience ID. """ + def test_get_audience__valid_id(self): + """ Test that audience object is retrieved correctly given a valid audience ID. """ - self.assertEqual(self.project_config.audience_id_map['11154'], - self.project_config.get_audience('11154')) + self.assertEqual( + self.project_config.audience_id_map['11154'], self.project_config.get_audience('11154'), + ) - def test_get_audience__invalid_id(self): - """ Test that None is returned for an invalid audience ID. """ + def test_get_audience__invalid_id(self): + """ Test that None is returned for an invalid audience ID. """ - self.assertIsNone(self.project_config.get_audience('42')) + self.assertIsNone(self.project_config.get_audience('42')) - def test_get_audience__prefers_typedAudiences_over_audiences(self): - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - config = opt_obj.config_manager.get_config() + def test_get_audience__prefers_typedAudiences_over_audiences(self): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) + config = opt_obj.config_manager.get_config() - audiences = self.config_dict_with_typed_audiences['audiences'] - typed_audiences = self.config_dict_with_typed_audiences['typedAudiences'] + audiences = self.config_dict_with_typed_audiences['audiences'] + typed_audiences = self.config_dict_with_typed_audiences['typedAudiences'] - audience_3988293898 = { - 'id': '3988293898', - 'name': '$$dummySubstringString', - 'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }' - } + audience_3988293898 = { + 'id': '3988293898', + 'name': '$$dummySubstringString', + 'conditions': '{ "type": "custom_attribute", "name": "$opt_dummy_attribute", "value": "impossible_value" }', + } - self.assertTrue(audience_3988293898 in audiences) + self.assertTrue(audience_3988293898 in audiences) - typed_audience_3988293898 = { - 'id': '3988293898', - 'name': 'substringString', - 'conditions': ['and', ['or', ['or', {'name': 'house', 'type': 'custom_attribute', - 'match': 'substring', 'value': 'Slytherin'}]]] - } + typed_audience_3988293898 = { + 'id': '3988293898', + 'name': 'substringString', + 'conditions': [ + 'and', + [ + 'or', + ['or', {'name': 'house', 'type': 'custom_attribute', 'match': 'substring', 'value': 'Slytherin'}], + ], + ], + } - self.assertTrue(typed_audience_3988293898 in typed_audiences) + self.assertTrue(typed_audience_3988293898 in typed_audiences) - audience = config.get_audience('3988293898') + audience = config.get_audience('3988293898') - self.assertEqual('3988293898', audience.id) - self.assertEqual('substringString', audience.name) + self.assertEqual('3988293898', audience.id) + self.assertEqual('substringString', audience.name) - # compare parsed JSON as conditions for typedAudiences is generated via json.dumps - # which can be different for python versions. - self.assertEqual(json.loads( - '["and", ["or", ["or", {"match": "substring", "type": "custom_attribute",' - ' "name": "house", "value": "Slytherin"}]]]'), - json.loads(audience.conditions) - ) + # compare parsed JSON as conditions for typedAudiences is generated via json.dumps + # which can be different for python versions. + self.assertEqual( + json.loads( + '["and", ["or", ["or", {"match": "substring", "type": "custom_attribute",' + ' "name": "house", "value": "Slytherin"}]]]' + ), + json.loads(audience.conditions), + ) - def test_get_variation_from_key__valid_experiment_key(self): - """ Test that variation is retrieved correctly when valid experiment key and variation key are provided. """ + def test_get_variation_from_key__valid_experiment_key(self): + """ Test that variation is retrieved correctly when valid experiment key and variation key are provided. """ - self.assertEqual(entities.Variation('111128', 'control'), - self.project_config.get_variation_from_key('test_experiment', 'control')) + self.assertEqual( + entities.Variation('111128', 'control'), + self.project_config.get_variation_from_key('test_experiment', 'control'), + ) - def test_get_variation_from_key__invalid_experiment_key(self): - """ Test that None is returned when provided experiment key is invalid. """ + def test_get_variation_from_key__invalid_experiment_key(self): + """ Test that None is returned when provided experiment key is invalid. """ - self.assertIsNone(self.project_config.get_variation_from_key('invalid_key', 'control')) + self.assertIsNone(self.project_config.get_variation_from_key('invalid_key', 'control')) - def test_get_variation_from_key__invalid_variation_key(self): - """ Test that None is returned when provided variation ID is invalid. """ + def test_get_variation_from_key__invalid_variation_key(self): + """ Test that None is returned when provided variation ID is invalid. """ - self.assertIsNone(self.project_config.get_variation_from_key('test_experiment', 'invalid_key')) + self.assertIsNone(self.project_config.get_variation_from_key('test_experiment', 'invalid_key')) - def test_get_variation_from_id__valid_experiment_key(self): - """ Test that variation is retrieved correctly when valid experiment key and variation ID are provided. """ + def test_get_variation_from_id__valid_experiment_key(self): + """ Test that variation is retrieved correctly when valid experiment key and variation ID are provided. """ - self.assertEqual(entities.Variation('111128', 'control'), - self.project_config.get_variation_from_id('test_experiment', '111128')) + self.assertEqual( + entities.Variation('111128', 'control'), + self.project_config.get_variation_from_id('test_experiment', '111128'), + ) - def test_get_variation_from_id__invalid_experiment_key(self): - """ Test that None is returned when provided experiment key is invalid. """ + def test_get_variation_from_id__invalid_experiment_key(self): + """ Test that None is returned when provided experiment key is invalid. """ - self.assertIsNone(self.project_config.get_variation_from_id('invalid_key', '111128')) + self.assertIsNone(self.project_config.get_variation_from_id('invalid_key', '111128')) - def test_get_variation_from_id__invalid_variation_key(self): - """ Test that None is returned when provided variation ID is invalid. """ + def test_get_variation_from_id__invalid_variation_key(self): + """ Test that None is returned when provided variation ID is invalid. """ - self.assertIsNone(self.project_config.get_variation_from_id('test_experiment', '42')) + self.assertIsNone(self.project_config.get_variation_from_id('test_experiment', '42')) - def test_get_event__valid_key(self): - """ Test that event is retrieved correctly for valid event key. """ + def test_get_event__valid_key(self): + """ Test that event is retrieved correctly for valid event key. """ - self.assertEqual(entities.Event('111095', 'test_event', ['111127']), - self.project_config.get_event('test_event')) + self.assertEqual( + entities.Event('111095', 'test_event', ['111127']), self.project_config.get_event('test_event'), + ) - def test_get_event__invalid_key(self): - """ Test that None is returned when provided goal key is invalid. """ + def test_get_event__invalid_key(self): + """ Test that None is returned when provided goal key is invalid. """ - self.assertIsNone(self.project_config.get_event('invalid_key')) + self.assertIsNone(self.project_config.get_event('invalid_key')) - def test_get_attribute_id__valid_key(self): - """ Test that attribute ID is retrieved correctly for valid attribute key. """ + def test_get_attribute_id__valid_key(self): + """ Test that attribute ID is retrieved correctly for valid attribute key. """ - self.assertEqual('111094', - self.project_config.get_attribute_id('test_attribute')) + self.assertEqual('111094', self.project_config.get_attribute_id('test_attribute')) - def test_get_attribute_id__invalid_key(self): - """ Test that None is returned when provided attribute key is invalid. """ + def test_get_attribute_id__invalid_key(self): + """ Test that None is returned when provided attribute key is invalid. """ - self.assertIsNone(self.project_config.get_attribute_id('invalid_key')) + self.assertIsNone(self.project_config.get_attribute_id('invalid_key')) - def test_get_attribute_id__reserved_key(self): - """ Test that Attribute Key is returned as ID when provided attribute key is reserved key. """ - self.assertEqual('$opt_user_agent', - self.project_config.get_attribute_id('$opt_user_agent')) + def test_get_attribute_id__reserved_key(self): + """ Test that Attribute Key is returned as ID when provided attribute key is reserved key. """ + self.assertEqual('$opt_user_agent', self.project_config.get_attribute_id('$opt_user_agent')) - def test_get_attribute_id__unknown_key_with_opt_prefix(self): - """ Test that Attribute Key is returned as ID when provided attribute key is not + def test_get_attribute_id__unknown_key_with_opt_prefix(self): + """ Test that Attribute Key is returned as ID when provided attribute key is not present in the datafile but has $opt prefix. """ - self.assertEqual('$opt_interesting', - self.project_config.get_attribute_id('$opt_interesting')) - - def test_get_group__valid_id(self): - """ Test that group is retrieved correctly for valid group ID. """ - - self.assertEqual(entities.Group(self.config_dict['groups'][0]['id'], - self.config_dict['groups'][0]['policy'], - self.config_dict['groups'][0]['experiments'], - self.config_dict['groups'][0]['trafficAllocation']), - self.project_config.get_group('19228')) - - def test_get_group__invalid_id(self): - """ Test that None is returned when provided group ID is invalid. """ - - self.assertIsNone(self.project_config.get_group('42')) - - def test_get_feature_from_key__valid_feature_key(self): - """ Test that a valid feature is returned given a valid feature key. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - project_config = opt_obj.config_manager.get_config() - - expected_feature = entities.FeatureFlag( - '91112', - 'test_feature_in_rollout', - [], - '211111', - { - 'is_running': entities.Variable('132', 'is_running', 'boolean', 'false'), - 'message': entities.Variable('133', 'message', 'string', 'Hello'), - 'price': entities.Variable('134', 'price', 'double', '99.99'), - 'count': entities.Variable('135', 'count', 'integer', '999') - } - ) - - self.assertEqual(expected_feature, project_config.get_feature_from_key('test_feature_in_rollout')) - - def test_get_feature_from_key__invalid_feature_key(self): - """ Test that None is returned given an invalid feature key. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - project_config = opt_obj.config_manager.get_config() - - self.assertIsNone(project_config.get_feature_from_key('invalid_feature_key')) - - def test_get_rollout_from_id__valid_rollout_id(self): - """ Test that a valid rollout is returned """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - project_config = opt_obj.config_manager.get_config() - - expected_rollout = entities.Layer('211111', [{ - 'id': '211127', - 'key': '211127', - 'status': 'Running', - 'forcedVariations': {}, - 'layerId': '211111', - 'audienceIds': ['11154'], - 'trafficAllocation': [{ - 'entityId': '211129', - 'endOfRange': 9000 - }], - 'variations': [{ - 'key': '211129', - 'id': '211129', - 'featureEnabled': True, - 'variables': [{ - 'id': '132', 'value': 'true' - }, { - 'id': '133', 'value': 'Hello audience' - }, { - 'id': '134', 'value': '39.99' - }, { - 'id': '135', 'value': '399' - }] - }, { - 'key': '211229', - 'id': '211229', - 'featureEnabled': False, - 'variables': [{ - 'id': '132', 'value': 'true' - }, { - 'id': '133', 'value': 'environment' - }, { - 'id': '134', 'value': '49.99' - }, { - 'id': '135', 'value': '499' - }] - }] - }, { - 'id': '211137', - 'key': '211137', - 'status': 'Running', - 'forcedVariations': {}, - 'layerId': '211111', - 'audienceIds': ['11159'], - 'trafficAllocation': [{ - 'entityId': '211139', - 'endOfRange': 3000 - }], - 'variations': [{ - 'key': '211139', - 'id': '211139', - 'featureEnabled': True - }] - }, { - 'id': '211147', - 'key': '211147', - 'status': 'Running', - 'forcedVariations': {}, - 'layerId': '211111', - 'audienceIds': [], - 'trafficAllocation': [{ - 'entityId': '211149', - 'endOfRange': 6000 - }], - 'variations': [{ - 'key': '211149', - 'id': '211149', - 'featureEnabled': True - }] - }]) - self.assertEqual(expected_rollout, project_config.get_rollout_from_id('211111')) - - def test_get_rollout_from_id__invalid_rollout_id(self): - """ Test that None is returned for an unknown Rollout ID """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features), - logger=logger.NoOpLogger()) - project_config = opt_obj.config_manager.get_config() - with mock.patch.object(project_config, 'logger') as mock_config_logging: - self.assertIsNone(project_config.get_rollout_from_id('aabbccdd')) - - mock_config_logging.error.assert_called_once_with('Rollout with ID "aabbccdd" is not in datafile.') - - def test_get_variable_value_for_variation__returns_valid_value(self): - """ Test that the right value is returned. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - project_config = opt_obj.config_manager.get_config() - - variation = project_config.get_variation_from_id('test_experiment', '111128') - is_working_variable = project_config.get_variable_for_feature('test_feature_in_experiment', 'is_working') - environment_variable = project_config.get_variable_for_feature('test_feature_in_experiment', 'environment') - self.assertEqual('false', project_config.get_variable_value_for_variation(is_working_variable, variation)) - self.assertEqual('prod', project_config.get_variable_value_for_variation(environment_variable, variation)) - - def test_get_variable_value_for_variation__invalid_variable(self): - """ Test that an invalid variable key will return None. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - project_config = opt_obj.config_manager.get_config() - - variation = project_config.get_variation_from_id('test_experiment', '111128') - self.assertIsNone(project_config.get_variable_value_for_variation(None, variation)) - - def test_get_variable_value_for_variation__no_variables_for_variation(self): - """ Test that a variation with no variables will return None. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - project_config = opt_obj.config_manager.get_config() - - variation = entities.Variation('1111281', 'invalid_variation', []) - is_working_variable = project_config.get_variable_for_feature('test_feature_in_experiment', 'is_working') - self.assertIsNone(project_config.get_variable_value_for_variation(is_working_variable, variation)) - - def test_get_variable_value_for_variation__no_usage_of_variable(self): - """ Test that a variable with no usage will return default value for variable. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - project_config = opt_obj.config_manager.get_config() - - variation = project_config.get_variation_from_id('test_experiment', '111128') - variable_without_usage_variable = project_config.get_variable_for_feature('test_feature_in_experiment', - 'variable_without_usage') - self.assertEqual('45', project_config.get_variable_value_for_variation(variable_without_usage_variable, variation)) - - def test_get_variable_for_feature__returns_valid_variable(self): - """ Test that the feature variable is returned. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - project_config = opt_obj.config_manager.get_config() - - variable = project_config.get_variable_for_feature('test_feature_in_experiment', 'is_working') - self.assertEqual(entities.Variable('127', 'is_working', 'boolean', 'true'), variable) - - def test_get_variable_for_feature__invalid_feature_key(self): - """ Test that an invalid feature key will return None. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - project_config = opt_obj.config_manager.get_config() - - self.assertIsNone(project_config.get_variable_for_feature('invalid_feature', 'is_working')) - - def test_get_variable_for_feature__invalid_variable_key(self): - """ Test that an invalid variable key will return None. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - project_config = opt_obj.config_manager.get_config() - - self.assertIsNone(project_config.get_variable_for_feature('test_feature_in_experiment', 'invalid_variable_key')) + self.assertEqual('$opt_interesting', self.project_config.get_attribute_id('$opt_interesting')) + + def test_get_group__valid_id(self): + """ Test that group is retrieved correctly for valid group ID. """ + + self.assertEqual( + entities.Group( + self.config_dict['groups'][0]['id'], + self.config_dict['groups'][0]['policy'], + self.config_dict['groups'][0]['experiments'], + self.config_dict['groups'][0]['trafficAllocation'], + ), + self.project_config.get_group('19228'), + ) + + def test_get_group__invalid_id(self): + """ Test that None is returned when provided group ID is invalid. """ + + self.assertIsNone(self.project_config.get_group('42')) + + def test_get_feature_from_key__valid_feature_key(self): + """ Test that a valid feature is returned given a valid feature key. """ + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() + + expected_feature = entities.FeatureFlag( + '91112', + 'test_feature_in_rollout', + [], + '211111', + { + 'is_running': entities.Variable('132', 'is_running', 'boolean', 'false'), + 'message': entities.Variable('133', 'message', 'string', 'Hello'), + 'price': entities.Variable('134', 'price', 'double', '99.99'), + 'count': entities.Variable('135', 'count', 'integer', '999'), + }, + ) + + self.assertEqual( + expected_feature, project_config.get_feature_from_key('test_feature_in_rollout'), + ) + + def test_get_feature_from_key__invalid_feature_key(self): + """ Test that None is returned given an invalid feature key. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() + + self.assertIsNone(project_config.get_feature_from_key('invalid_feature_key')) + + def test_get_rollout_from_id__valid_rollout_id(self): + """ Test that a valid rollout is returned """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() + + expected_rollout = entities.Layer( + '211111', + [ + { + 'id': '211127', + 'key': '211127', + 'status': 'Running', + 'forcedVariations': {}, + 'layerId': '211111', + 'audienceIds': ['11154'], + 'trafficAllocation': [{'entityId': '211129', 'endOfRange': 9000}], + 'variations': [ + { + 'key': '211129', + 'id': '211129', + 'featureEnabled': True, + 'variables': [ + {'id': '132', 'value': 'true'}, + {'id': '133', 'value': 'Hello audience'}, + {'id': '134', 'value': '39.99'}, + {'id': '135', 'value': '399'}, + ], + }, + { + 'key': '211229', + 'id': '211229', + 'featureEnabled': False, + 'variables': [ + {'id': '132', 'value': 'true'}, + {'id': '133', 'value': 'environment'}, + {'id': '134', 'value': '49.99'}, + {'id': '135', 'value': '499'}, + ], + }, + ], + }, + { + 'id': '211137', + 'key': '211137', + 'status': 'Running', + 'forcedVariations': {}, + 'layerId': '211111', + 'audienceIds': ['11159'], + 'trafficAllocation': [{'entityId': '211139', 'endOfRange': 3000}], + 'variations': [{'key': '211139', 'id': '211139', 'featureEnabled': True}], + }, + { + 'id': '211147', + 'key': '211147', + 'status': 'Running', + 'forcedVariations': {}, + 'layerId': '211111', + 'audienceIds': [], + 'trafficAllocation': [{'entityId': '211149', 'endOfRange': 6000}], + 'variations': [{'key': '211149', 'id': '211149', 'featureEnabled': True}], + }, + ], + ) + self.assertEqual(expected_rollout, project_config.get_rollout_from_id('211111')) + + def test_get_rollout_from_id__invalid_rollout_id(self): + """ Test that None is returned for an unknown Rollout ID """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features), logger=logger.NoOpLogger()) + project_config = opt_obj.config_manager.get_config() + with mock.patch.object(project_config, 'logger') as mock_config_logging: + self.assertIsNone(project_config.get_rollout_from_id('aabbccdd')) + + mock_config_logging.error.assert_called_once_with('Rollout with ID "aabbccdd" is not in datafile.') + + def test_get_variable_value_for_variation__returns_valid_value(self): + """ Test that the right value is returned. """ + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() + + variation = project_config.get_variation_from_id('test_experiment', '111128') + is_working_variable = project_config.get_variable_for_feature('test_feature_in_experiment', 'is_working') + environment_variable = project_config.get_variable_for_feature('test_feature_in_experiment', 'environment') + self.assertEqual( + 'false', project_config.get_variable_value_for_variation(is_working_variable, variation), + ) + self.assertEqual( + 'prod', project_config.get_variable_value_for_variation(environment_variable, variation), + ) + + def test_get_variable_value_for_variation__invalid_variable(self): + """ Test that an invalid variable key will return None. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() + + variation = project_config.get_variation_from_id('test_experiment', '111128') + self.assertIsNone(project_config.get_variable_value_for_variation(None, variation)) + + def test_get_variable_value_for_variation__no_variables_for_variation(self): + """ Test that a variation with no variables will return None. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() + + variation = entities.Variation('1111281', 'invalid_variation', []) + is_working_variable = project_config.get_variable_for_feature('test_feature_in_experiment', 'is_working') + self.assertIsNone(project_config.get_variable_value_for_variation(is_working_variable, variation)) + + def test_get_variable_value_for_variation__no_usage_of_variable(self): + """ Test that a variable with no usage will return default value for variable. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() + + variation = project_config.get_variation_from_id('test_experiment', '111128') + variable_without_usage_variable = project_config.get_variable_for_feature( + 'test_feature_in_experiment', 'variable_without_usage' + ) + self.assertEqual( + '45', project_config.get_variable_value_for_variation(variable_without_usage_variable, variation), + ) + + def test_get_variable_for_feature__returns_valid_variable(self): + """ Test that the feature variable is returned. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() + + variable = project_config.get_variable_for_feature('test_feature_in_experiment', 'is_working') + self.assertEqual(entities.Variable('127', 'is_working', 'boolean', 'true'), variable) + + def test_get_variable_for_feature__invalid_feature_key(self): + """ Test that an invalid feature key will return None. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() + + self.assertIsNone(project_config.get_variable_for_feature('invalid_feature', 'is_working')) + + def test_get_variable_for_feature__invalid_variable_key(self): + """ Test that an invalid variable key will return None. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() + + self.assertIsNone(project_config.get_variable_for_feature('test_feature_in_experiment', 'invalid_variable_key')) class ConfigLoggingTest(base.BaseTest): + def setUp(self): + base.BaseTest.setUp(self) + self.optimizely = optimizely.Optimizely(json.dumps(self.config_dict), logger=logger.SimpleLogger()) + self.project_config = self.optimizely.config_manager.get_config() - def setUp(self): - base.BaseTest.setUp(self) - self.optimizely = optimizely.Optimizely(json.dumps(self.config_dict), - logger=logger.SimpleLogger()) - self.project_config = self.optimizely.config_manager.get_config() - - def test_get_experiment_from_key__invalid_key(self): - """ Test that message is logged when provided experiment key is invalid. """ + def test_get_experiment_from_key__invalid_key(self): + """ Test that message is logged when provided experiment key is invalid. """ - with mock.patch.object(self.project_config, 'logger') as mock_config_logging: - self.project_config.get_experiment_from_key('invalid_key') + with mock.patch.object(self.project_config, 'logger') as mock_config_logging: + self.project_config.get_experiment_from_key('invalid_key') - mock_config_logging.error.assert_called_once_with('Experiment key "invalid_key" is not in datafile.') + mock_config_logging.error.assert_called_once_with('Experiment key "invalid_key" is not in datafile.') - def test_get_audience__invalid_id(self): - """ Test that message is logged when provided audience ID is invalid. """ + def test_get_audience__invalid_id(self): + """ Test that message is logged when provided audience ID is invalid. """ - with mock.patch.object(self.project_config, 'logger') as mock_config_logging: - self.project_config.get_audience('42') + with mock.patch.object(self.project_config, 'logger') as mock_config_logging: + self.project_config.get_audience('42') - mock_config_logging.error.assert_called_once_with('Audience ID "42" is not in datafile.') + mock_config_logging.error.assert_called_once_with('Audience ID "42" is not in datafile.') - def test_get_variation_from_key__invalid_experiment_key(self): - """ Test that message is logged when provided experiment key is invalid. """ + def test_get_variation_from_key__invalid_experiment_key(self): + """ Test that message is logged when provided experiment key is invalid. """ - with mock.patch.object(self.project_config, 'logger') as mock_config_logging: - self.project_config.get_variation_from_key('invalid_key', 'control') + with mock.patch.object(self.project_config, 'logger') as mock_config_logging: + self.project_config.get_variation_from_key('invalid_key', 'control') - mock_config_logging.error.assert_called_once_with('Experiment key "invalid_key" is not in datafile.') + mock_config_logging.error.assert_called_once_with('Experiment key "invalid_key" is not in datafile.') - def test_get_variation_from_key__invalid_variation_key(self): - """ Test that message is logged when provided variation key is invalid. """ + def test_get_variation_from_key__invalid_variation_key(self): + """ Test that message is logged when provided variation key is invalid. """ - with mock.patch.object(self.project_config, 'logger') as mock_config_logging: - self.project_config.get_variation_from_key('test_experiment', 'invalid_key') + with mock.patch.object(self.project_config, 'logger') as mock_config_logging: + self.project_config.get_variation_from_key('test_experiment', 'invalid_key') - mock_config_logging.error.assert_called_once_with('Variation key "invalid_key" is not in datafile.') + mock_config_logging.error.assert_called_once_with('Variation key "invalid_key" is not in datafile.') - def test_get_variation_from_id__invalid_experiment_key(self): - """ Test that message is logged when provided experiment key is invalid. """ + def test_get_variation_from_id__invalid_experiment_key(self): + """ Test that message is logged when provided experiment key is invalid. """ - with mock.patch.object(self.project_config, 'logger') as mock_config_logging: - self.project_config.get_variation_from_id('invalid_key', '111128') + with mock.patch.object(self.project_config, 'logger') as mock_config_logging: + self.project_config.get_variation_from_id('invalid_key', '111128') - mock_config_logging.error.assert_called_once_with('Experiment key "invalid_key" is not in datafile.') + mock_config_logging.error.assert_called_once_with('Experiment key "invalid_key" is not in datafile.') - def test_get_variation_from_id__invalid_variation_id(self): - """ Test that message is logged when provided variation ID is invalid. """ + def test_get_variation_from_id__invalid_variation_id(self): + """ Test that message is logged when provided variation ID is invalid. """ - with mock.patch.object(self.project_config, 'logger') as mock_config_logging: - self.project_config.get_variation_from_id('test_experiment', '42') + with mock.patch.object(self.project_config, 'logger') as mock_config_logging: + self.project_config.get_variation_from_id('test_experiment', '42') - mock_config_logging.error.assert_called_once_with('Variation ID "42" is not in datafile.') + mock_config_logging.error.assert_called_once_with('Variation ID "42" is not in datafile.') - def test_get_event__invalid_key(self): - """ Test that message is logged when provided event key is invalid. """ + def test_get_event__invalid_key(self): + """ Test that message is logged when provided event key is invalid. """ - with mock.patch.object(self.project_config, 'logger') as mock_config_logging: - self.project_config.get_event('invalid_key') + with mock.patch.object(self.project_config, 'logger') as mock_config_logging: + self.project_config.get_event('invalid_key') - mock_config_logging.error.assert_called_once_with('Event "invalid_key" is not in datafile.') + mock_config_logging.error.assert_called_once_with('Event "invalid_key" is not in datafile.') - def test_get_attribute_id__invalid_key(self): - """ Test that message is logged when provided attribute key is invalid. """ + def test_get_attribute_id__invalid_key(self): + """ Test that message is logged when provided attribute key is invalid. """ - with mock.patch.object(self.project_config, 'logger') as mock_config_logging: - self.project_config.get_attribute_id('invalid_key') + with mock.patch.object(self.project_config, 'logger') as mock_config_logging: + self.project_config.get_attribute_id('invalid_key') - mock_config_logging.error.assert_called_once_with('Attribute "invalid_key" is not in datafile.') + mock_config_logging.error.assert_called_once_with('Attribute "invalid_key" is not in datafile.') - def test_get_attribute_id__key_with_opt_prefix_but_not_a_control_attribute(self): - """ Test that message is logged when provided attribute key has $opt_ in prefix and + def test_get_attribute_id__key_with_opt_prefix_but_not_a_control_attribute(self): + """ Test that message is logged when provided attribute key has $opt_ in prefix and key is not one of the control attributes. """ - self.project_config.attribute_key_map['$opt_abc'] = entities.Attribute('007', '$opt_abc') + self.project_config.attribute_key_map['$opt_abc'] = entities.Attribute('007', '$opt_abc') - with mock.patch.object(self.project_config, 'logger') as mock_config_logging: - self.project_config.get_attribute_id('$opt_abc') + with mock.patch.object(self.project_config, 'logger') as mock_config_logging: + self.project_config.get_attribute_id('$opt_abc') - mock_config_logging.warning.assert_called_once_with(("Attribute $opt_abc unexpectedly has reserved prefix $opt_; " - "using attribute ID instead of reserved attribute name.")) + mock_config_logging.warning.assert_called_once_with( + ( + "Attribute $opt_abc unexpectedly has reserved prefix $opt_; " + "using attribute ID instead of reserved attribute name." + ) + ) - def test_get_group__invalid_id(self): - """ Test that message is logged when provided group ID is invalid. """ + def test_get_group__invalid_id(self): + """ Test that message is logged when provided group ID is invalid. """ - with mock.patch.object(self.project_config, 'logger') as mock_config_logging: - self.project_config.get_group('42') + with mock.patch.object(self.project_config, 'logger') as mock_config_logging: + self.project_config.get_group('42') - mock_config_logging.error.assert_called_once_with('Group ID "42" is not in datafile.') + mock_config_logging.error.assert_called_once_with('Group ID "42" is not in datafile.') class ConfigExceptionTest(base.BaseTest): - - def setUp(self): - base.BaseTest.setUp(self) - self.optimizely = optimizely.Optimizely(json.dumps(self.config_dict), - error_handler=error_handler.RaiseExceptionErrorHandler) - self.project_config = self.optimizely.config_manager.get_config() - - def test_get_experiment_from_key__invalid_key(self): - """ Test that exception is raised when provided experiment key is invalid. """ - - self.assertRaisesRegexp(exceptions.InvalidExperimentException, - enums.Errors.INVALID_EXPERIMENT_KEY, - self.project_config.get_experiment_from_key, 'invalid_key') - - def test_get_audience__invalid_id(self): - """ Test that message is logged when provided audience ID is invalid. """ - - self.assertRaisesRegexp(exceptions.InvalidAudienceException, - enums.Errors.INVALID_AUDIENCE, - self.project_config.get_audience, '42') - - def test_get_variation_from_key__invalid_experiment_key(self): - """ Test that exception is raised when provided experiment key is invalid. """ - - self.assertRaisesRegexp(exceptions.InvalidExperimentException, - enums.Errors.INVALID_EXPERIMENT_KEY, - self.project_config.get_variation_from_key, 'invalid_key', 'control') - - def test_get_variation_from_key__invalid_variation_key(self): - """ Test that exception is raised when provided variation key is invalid. """ - - self.assertRaisesRegexp(exceptions.InvalidVariationException, - enums.Errors.INVALID_VARIATION, - self.project_config.get_variation_from_key, 'test_experiment', 'invalid_key') - - def test_get_variation_from_id__invalid_experiment_key(self): - """ Test that exception is raised when provided experiment key is invalid. """ - - self.assertRaisesRegexp(exceptions.InvalidExperimentException, - enums.Errors.INVALID_EXPERIMENT_KEY, - self.project_config.get_variation_from_id, 'invalid_key', '111128') - - def test_get_variation_from_id__invalid_variation_id(self): - """ Test that exception is raised when provided variation ID is invalid. """ - - self.assertRaisesRegexp(exceptions.InvalidVariationException, - enums.Errors.INVALID_VARIATION, - self.project_config.get_variation_from_key, 'test_experiment', '42') - - def test_get_event__invalid_key(self): - """ Test that exception is raised when provided event key is invalid. """ - - self.assertRaisesRegexp(exceptions.InvalidEventException, - enums.Errors.INVALID_EVENT_KEY, - self.project_config.get_event, 'invalid_key') - - def test_get_attribute_id__invalid_key(self): - """ Test that exception is raised when provided attribute key is invalid. """ - - self.assertRaisesRegexp(exceptions.InvalidAttributeException, - enums.Errors.INVALID_ATTRIBUTE, - self.project_config.get_attribute_id, 'invalid_key') - - def test_get_group__invalid_id(self): - """ Test that exception is raised when provided group ID is invalid. """ - - self.assertRaisesRegexp(exceptions.InvalidGroupException, - enums.Errors.INVALID_GROUP_ID, - self.project_config.get_group, '42') - - def test_is_feature_experiment(self): - """ Test that a true is returned if experiment is a feature test, false otherwise. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - project_config = opt_obj.config_manager.get_config() - - experiment = project_config.get_experiment_from_key('test_experiment2') - feature_experiment = project_config.get_experiment_from_key('test_experiment') - - self.assertStrictFalse(project_config.is_feature_experiment(experiment.id)) - self.assertStrictTrue(project_config.is_feature_experiment(feature_experiment.id)) + def setUp(self): + base.BaseTest.setUp(self) + self.optimizely = optimizely.Optimizely( + json.dumps(self.config_dict), error_handler=error_handler.RaiseExceptionErrorHandler, + ) + self.project_config = self.optimizely.config_manager.get_config() + + def test_get_experiment_from_key__invalid_key(self): + """ Test that exception is raised when provided experiment key is invalid. """ + + self.assertRaisesRegexp( + exceptions.InvalidExperimentException, + enums.Errors.INVALID_EXPERIMENT_KEY, + self.project_config.get_experiment_from_key, + 'invalid_key', + ) + + def test_get_audience__invalid_id(self): + """ Test that message is logged when provided audience ID is invalid. """ + + self.assertRaisesRegexp( + exceptions.InvalidAudienceException, enums.Errors.INVALID_AUDIENCE, self.project_config.get_audience, '42', + ) + + def test_get_variation_from_key__invalid_experiment_key(self): + """ Test that exception is raised when provided experiment key is invalid. """ + + self.assertRaisesRegexp( + exceptions.InvalidExperimentException, + enums.Errors.INVALID_EXPERIMENT_KEY, + self.project_config.get_variation_from_key, + 'invalid_key', + 'control', + ) + + def test_get_variation_from_key__invalid_variation_key(self): + """ Test that exception is raised when provided variation key is invalid. """ + + self.assertRaisesRegexp( + exceptions.InvalidVariationException, + enums.Errors.INVALID_VARIATION, + self.project_config.get_variation_from_key, + 'test_experiment', + 'invalid_key', + ) + + def test_get_variation_from_id__invalid_experiment_key(self): + """ Test that exception is raised when provided experiment key is invalid. """ + + self.assertRaisesRegexp( + exceptions.InvalidExperimentException, + enums.Errors.INVALID_EXPERIMENT_KEY, + self.project_config.get_variation_from_id, + 'invalid_key', + '111128', + ) + + def test_get_variation_from_id__invalid_variation_id(self): + """ Test that exception is raised when provided variation ID is invalid. """ + + self.assertRaisesRegexp( + exceptions.InvalidVariationException, + enums.Errors.INVALID_VARIATION, + self.project_config.get_variation_from_key, + 'test_experiment', + '42', + ) + + def test_get_event__invalid_key(self): + """ Test that exception is raised when provided event key is invalid. """ + + self.assertRaisesRegexp( + exceptions.InvalidEventException, + enums.Errors.INVALID_EVENT_KEY, + self.project_config.get_event, + 'invalid_key', + ) + + def test_get_attribute_id__invalid_key(self): + """ Test that exception is raised when provided attribute key is invalid. """ + + self.assertRaisesRegexp( + exceptions.InvalidAttributeException, + enums.Errors.INVALID_ATTRIBUTE, + self.project_config.get_attribute_id, + 'invalid_key', + ) + + def test_get_group__invalid_id(self): + """ Test that exception is raised when provided group ID is invalid. """ + + self.assertRaisesRegexp( + exceptions.InvalidGroupException, enums.Errors.INVALID_GROUP_ID, self.project_config.get_group, '42', + ) + + def test_is_feature_experiment(self): + """ Test that a true is returned if experiment is a feature test, false otherwise. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() + + experiment = project_config.get_experiment_from_key('test_experiment2') + feature_experiment = project_config.get_experiment_from_key('test_experiment') + + self.assertStrictFalse(project_config.is_feature_experiment(experiment.id)) + self.assertStrictTrue(project_config.is_feature_experiment(feature_experiment.id)) diff --git a/tests/test_config_manager.py b/tests/test_config_manager.py index 38be849d..c7425f4c 100644 --- a/tests/test_config_manager.py +++ b/tests/test_config_manager.py @@ -27,26 +27,35 @@ class StaticConfigManagerTest(base.BaseTest): def test_init__invalid_logger_fails(self): """ Test that initialization fails if logger is invalid. """ + class InvalidLogger(object): pass - with self.assertRaisesRegexp(optimizely_exceptions.InvalidInputException, - 'Provided "logger" is in an invalid format.'): + + with self.assertRaisesRegexp( + optimizely_exceptions.InvalidInputException, 'Provided "logger" is in an invalid format.', + ): config_manager.StaticConfigManager(logger=InvalidLogger()) def test_init__invalid_error_handler_fails(self): """ Test that initialization fails if error_handler is invalid. """ + class InvalidErrorHandler(object): pass - with self.assertRaisesRegexp(optimizely_exceptions.InvalidInputException, - 'Provided "error_handler" is in an invalid format.'): + + with self.assertRaisesRegexp( + optimizely_exceptions.InvalidInputException, 'Provided "error_handler" is in an invalid format.', + ): config_manager.StaticConfigManager(error_handler=InvalidErrorHandler()) def test_init__invalid_notification_center_fails(self): """ Test that initialization fails if notification_center is invalid. """ + class InvalidNotificationCenter(object): pass - with self.assertRaisesRegexp(optimizely_exceptions.InvalidInputException, - 'Provided "notification_center" is in an invalid format.'): + + with self.assertRaisesRegexp( + optimizely_exceptions.InvalidInputException, 'Provided "notification_center" is in an invalid format.', + ): config_manager.StaticConfigManager(notification_center=InvalidNotificationCenter()) def test_set_config__success(self): @@ -56,13 +65,14 @@ def test_set_config__success(self): mock_notification_center = mock.Mock() with mock.patch('optimizely.config_manager.BaseConfigManager._validate_instantiation_options'): - project_config_manager = config_manager.StaticConfigManager(datafile=test_datafile, - logger=mock_logger, - notification_center=mock_notification_center) + project_config_manager = config_manager.StaticConfigManager( + datafile=test_datafile, logger=mock_logger, notification_center=mock_notification_center, + ) project_config_manager._set_config(test_datafile) - mock_logger.debug.assert_called_with('Received new datafile and updated config. ' - 'Old revision number: None. New revision number: 1.') + mock_logger.debug.assert_called_with( + 'Received new datafile and updated config. ' 'Old revision number: None. New revision number: 1.' + ) mock_notification_center.send_notifications.assert_called_once_with('OPTIMIZELY_CONFIG_UPDATE') def test_set_config__twice(self): @@ -72,13 +82,14 @@ def test_set_config__twice(self): mock_notification_center = mock.Mock() with mock.patch('optimizely.config_manager.BaseConfigManager._validate_instantiation_options'): - project_config_manager = config_manager.StaticConfigManager(datafile=test_datafile, - logger=mock_logger, - notification_center=mock_notification_center) + project_config_manager = config_manager.StaticConfigManager( + datafile=test_datafile, logger=mock_logger, notification_center=mock_notification_center, + ) project_config_manager._set_config(test_datafile) - mock_logger.debug.assert_called_with('Received new datafile and updated config. ' - 'Old revision number: None. New revision number: 1.') + mock_logger.debug.assert_called_with( + 'Received new datafile and updated config. ' 'Old revision number: None. New revision number: 1.' + ) self.assertEqual(1, mock_logger.debug.call_count) mock_notification_center.send_notifications.assert_called_once_with('OPTIMIZELY_CONFIG_UPDATE') @@ -98,18 +109,13 @@ def test_set_config__schema_validation(self): # Test that schema is validated. # Note: set_config is called in __init__ itself. - with mock.patch('optimizely.helpers.validator.is_datafile_valid', - return_value=True) as mock_validate_datafile: - config_manager.StaticConfigManager(datafile=test_datafile, - logger=mock_logger) + with mock.patch('optimizely.helpers.validator.is_datafile_valid', return_value=True) as mock_validate_datafile: + config_manager.StaticConfigManager(datafile=test_datafile, logger=mock_logger) mock_validate_datafile.assert_called_once_with(test_datafile) # Test that schema is not validated if skip_json_validation option is set to True. - with mock.patch('optimizely.helpers.validator.is_datafile_valid', - return_value=True) as mock_validate_datafile: - config_manager.StaticConfigManager(datafile=test_datafile, - logger=mock_logger, - skip_json_validation=True) + with mock.patch('optimizely.helpers.validator.is_datafile_valid', return_value=True) as mock_validate_datafile: + config_manager.StaticConfigManager(datafile=test_datafile, logger=mock_logger, skip_json_validation=True) mock_validate_datafile.assert_not_called() def test_set_config__unsupported_datafile_version(self): @@ -120,9 +126,9 @@ def test_set_config__unsupported_datafile_version(self): mock_notification_center = mock.Mock() with mock.patch('optimizely.config_manager.BaseConfigManager._validate_instantiation_options'): - project_config_manager = config_manager.StaticConfigManager(datafile=test_datafile, - logger=mock_logger, - notification_center=mock_notification_center) + project_config_manager = config_manager.StaticConfigManager( + datafile=test_datafile, logger=mock_logger, notification_center=mock_notification_center, + ) invalid_version_datafile = self.config_dict_with_features.copy() invalid_version_datafile['version'] = 'invalid_version' @@ -130,8 +136,9 @@ def test_set_config__unsupported_datafile_version(self): # Call set_config with datafile having invalid version project_config_manager._set_config(test_datafile) - mock_logger.error.assert_called_once_with('This version of the Python SDK does not support ' - 'the given datafile version: "invalid_version".') + mock_logger.error.assert_called_once_with( + 'This version of the Python SDK does not support ' 'the given datafile version: "invalid_version".' + ) self.assertEqual(0, mock_notification_center.call_count) def test_set_config__invalid_datafile(self): @@ -142,9 +149,9 @@ def test_set_config__invalid_datafile(self): mock_notification_center = mock.Mock() with mock.patch('optimizely.config_manager.BaseConfigManager._validate_instantiation_options'): - project_config_manager = config_manager.StaticConfigManager(datafile=test_datafile, - logger=mock_logger, - notification_center=mock_notification_center) + project_config_manager = config_manager.StaticConfigManager( + datafile=test_datafile, logger=mock_logger, notification_center=mock_notification_center, + ) # Call set_config with invalid content project_config_manager._set_config('invalid_datafile') @@ -162,8 +169,7 @@ def test_get_config(self): def test_get_config_blocks(self): """ Test that get_config blocks until blocking timeout is hit. """ start_time = time.time() - project_config_manager = config_manager.PollingConfigManager(sdk_key='sdk_key', - blocking_timeout=5) + project_config_manager = config_manager.PollingConfigManager(sdk_key='sdk_key', blocking_timeout=5) # Assert get_config should block until blocking timeout. project_config_manager.get_config() end_time = time.time() @@ -174,45 +180,64 @@ def test_get_config_blocks(self): class PollingConfigManagerTest(base.BaseTest): def test_init__no_sdk_key_no_url__fails(self, _): """ Test that initialization fails if there is no sdk_key or url provided. """ - self.assertRaisesRegexp(optimizely_exceptions.InvalidInputException, - 'Must provide at least one of sdk_key or url.', - config_manager.PollingConfigManager, sdk_key=None, url=None) + self.assertRaisesRegexp( + optimizely_exceptions.InvalidInputException, + 'Must provide at least one of sdk_key or url.', + config_manager.PollingConfigManager, + sdk_key=None, + url=None, + ) def test_get_datafile_url__no_sdk_key_no_url_raises(self, _): """ Test that get_datafile_url raises exception if no sdk_key or url is provided. """ - self.assertRaisesRegexp(optimizely_exceptions.InvalidInputException, - 'Must provide at least one of sdk_key or url.', - config_manager.PollingConfigManager.get_datafile_url, None, None, 'url_template') + self.assertRaisesRegexp( + optimizely_exceptions.InvalidInputException, + 'Must provide at least one of sdk_key or url.', + config_manager.PollingConfigManager.get_datafile_url, + None, + None, + 'url_template', + ) def test_get_datafile_url__invalid_url_template_raises(self, _): """ Test that get_datafile_url raises if url_template is invalid. """ # No url_template provided - self.assertRaisesRegexp(optimizely_exceptions.InvalidInputException, - 'Invalid url_template None provided', - config_manager.PollingConfigManager.get_datafile_url, 'optly_datafile_key', None, None) + self.assertRaisesRegexp( + optimizely_exceptions.InvalidInputException, + 'Invalid url_template None provided', + config_manager.PollingConfigManager.get_datafile_url, + 'optly_datafile_key', + None, + None, + ) # Incorrect url_template provided test_url_template = 'invalid_url_template_without_sdk_key_field_{key}' - self.assertRaisesRegexp(optimizely_exceptions.InvalidInputException, - 'Invalid url_template {} provided'.format(test_url_template), - config_manager.PollingConfigManager.get_datafile_url, - 'optly_datafile_key', None, test_url_template) + self.assertRaisesRegexp( + optimizely_exceptions.InvalidInputException, + 'Invalid url_template {} provided'.format(test_url_template), + config_manager.PollingConfigManager.get_datafile_url, + 'optly_datafile_key', + None, + test_url_template, + ) def test_get_datafile_url__sdk_key_and_template_provided(self, _): """ Test get_datafile_url when sdk_key and template are provided. """ test_sdk_key = 'optly_key' test_url_template = 'www.optimizelydatafiles.com/{sdk_key}.json' expected_url = test_url_template.format(sdk_key=test_sdk_key) - self.assertEqual(expected_url, - config_manager.PollingConfigManager.get_datafile_url(test_sdk_key, None, test_url_template)) + self.assertEqual( + expected_url, config_manager.PollingConfigManager.get_datafile_url(test_sdk_key, None, test_url_template), + ) def test_get_datafile_url__url_and_template_provided(self, _): """ Test get_datafile_url when url and url_template are provided. """ test_url_template = 'www.optimizelydatafiles.com/{sdk_key}.json' test_url = 'www.myoptimizelydatafiles.com/my_key.json' - self.assertEqual(test_url, config_manager.PollingConfigManager.get_datafile_url(None, - test_url, - test_url_template)) + self.assertEqual( + test_url, config_manager.PollingConfigManager.get_datafile_url(None, test_url, test_url_template), + ) def test_get_datafile_url__sdk_key_and_url_and_template_provided(self, _): """ Test get_datafile_url when sdk_key, url and url_template are provided. """ @@ -221,27 +246,32 @@ def test_get_datafile_url__sdk_key_and_url_and_template_provided(self, _): test_url = 'www.myoptimizelydatafiles.com/my_key.json' # Assert that if url is provided, it is always returned - self.assertEqual(test_url, config_manager.PollingConfigManager.get_datafile_url(test_sdk_key, - test_url, - test_url_template)) + self.assertEqual( + test_url, config_manager.PollingConfigManager.get_datafile_url(test_sdk_key, test_url, test_url_template), + ) def test_set_update_interval(self, _): """ Test set_update_interval with different inputs. """ with mock.patch('optimizely.config_manager.PollingConfigManager.fetch_datafile'): - project_config_manager = config_manager.PollingConfigManager(sdk_key='some_key') + project_config_manager = config_manager.PollingConfigManager(sdk_key='some_key') # Assert that if invalid update_interval is set, then exception is raised. - with self.assertRaisesRegexp(optimizely_exceptions.InvalidInputException, - 'Invalid update_interval "invalid interval" provided.'): + with self.assertRaisesRegexp( + optimizely_exceptions.InvalidInputException, 'Invalid update_interval "invalid interval" provided.', + ): project_config_manager.set_update_interval('invalid interval') # Assert that update_interval cannot be set to less than allowed minimum and instead is set to default value. project_config_manager.set_update_interval(-4.2) - self.assertEqual(enums.ConfigManager.DEFAULT_UPDATE_INTERVAL, project_config_manager.update_interval) + self.assertEqual( + enums.ConfigManager.DEFAULT_UPDATE_INTERVAL, project_config_manager.update_interval, + ) # Assert that if no update_interval is provided, it is set to default value. project_config_manager.set_update_interval(None) - self.assertEqual(enums.ConfigManager.DEFAULT_UPDATE_INTERVAL, project_config_manager.update_interval) + self.assertEqual( + enums.ConfigManager.DEFAULT_UPDATE_INTERVAL, project_config_manager.update_interval, + ) # Assert that if valid update_interval is provided, it is set to that value. project_config_manager.set_update_interval(42) @@ -250,16 +280,19 @@ def test_set_update_interval(self, _): def test_set_blocking_timeout(self, _): """ Test set_blocking_timeout with different inputs. """ with mock.patch('optimizely.config_manager.PollingConfigManager.fetch_datafile'): - project_config_manager = config_manager.PollingConfigManager(sdk_key='some_key') + project_config_manager = config_manager.PollingConfigManager(sdk_key='some_key') # Assert that if invalid blocking_timeout is set, then exception is raised. - with self.assertRaisesRegexp(optimizely_exceptions.InvalidInputException, - 'Invalid blocking timeout "invalid timeout" provided.'): + with self.assertRaisesRegexp( + optimizely_exceptions.InvalidInputException, 'Invalid blocking timeout "invalid timeout" provided.', + ): project_config_manager.set_blocking_timeout('invalid timeout') # Assert that blocking_timeout cannot be set to less than allowed minimum and instead is set to default value. project_config_manager.set_blocking_timeout(-4) - self.assertEqual(enums.ConfigManager.DEFAULT_BLOCKING_TIMEOUT, project_config_manager.blocking_timeout) + self.assertEqual( + enums.ConfigManager.DEFAULT_BLOCKING_TIMEOUT, project_config_manager.blocking_timeout, + ) # Assert that blocking_timeout can be set to 0. project_config_manager.set_blocking_timeout(0) @@ -267,7 +300,9 @@ def test_set_blocking_timeout(self, _): # Assert that if no blocking_timeout is provided, it is set to default value. project_config_manager.set_blocking_timeout(None) - self.assertEqual(enums.ConfigManager.DEFAULT_BLOCKING_TIMEOUT, project_config_manager.blocking_timeout) + self.assertEqual( + enums.ConfigManager.DEFAULT_BLOCKING_TIMEOUT, project_config_manager.blocking_timeout, + ) # Assert that if valid blocking_timeout is provided, it is set to that value. project_config_manager.set_blocking_timeout(5) @@ -276,12 +311,12 @@ def test_set_blocking_timeout(self, _): def test_set_last_modified(self, _): """ Test that set_last_modified sets last_modified field based on header. """ with mock.patch('optimizely.config_manager.PollingConfigManager.fetch_datafile'): - project_config_manager = config_manager.PollingConfigManager(sdk_key='some_key') + project_config_manager = config_manager.PollingConfigManager(sdk_key='some_key') last_modified_time = 'Test Last Modified Time' test_response_headers = { 'Last-Modified': last_modified_time, - 'Some-Other-Important-Header': 'some_value' + 'Some-Other-Important-Header': 'some_value', } project_config_manager.set_last_modified(test_response_headers) self.assertEqual(last_modified_time, project_config_manager.last_modified) @@ -291,9 +326,7 @@ def test_fetch_datafile(self, _): with mock.patch('optimizely.config_manager.PollingConfigManager.fetch_datafile'): project_config_manager = config_manager.PollingConfigManager(sdk_key='some_key') expected_datafile_url = 'https://cdn.optimizely.com/datafiles/some_key.json' - test_headers = { - 'Last-Modified': 'New Time' - } + test_headers = {'Last-Modified': 'New Time'} test_datafile = json.dumps(self.config_dict_with_features) test_response = requests.Response() test_response.status_code = 200 @@ -309,9 +342,11 @@ def test_fetch_datafile(self, _): with mock.patch('requests.get', return_value=test_response) as mock_requests: project_config_manager.fetch_datafile() - mock_requests.assert_called_once_with(expected_datafile_url, - headers={'If-Modified-Since': test_headers['Last-Modified']}, - timeout=enums.ConfigManager.REQUEST_TIMEOUT) + mock_requests.assert_called_once_with( + expected_datafile_url, + headers={'If-Modified-Since': test_headers['Last-Modified']}, + timeout=enums.ConfigManager.REQUEST_TIMEOUT, + ) self.assertEqual(test_headers['Last-Modified'], project_config_manager.last_modified) self.assertIsInstance(project_config_manager.get_config(), project_config.ProjectConfig) diff --git a/tests/test_decision_service.py b/tests/test_decision_service.py index 84a8fd69..0812368a 100644 --- a/tests/test_decision_service.py +++ b/tests/test_decision_service.py @@ -23,879 +23,1372 @@ class DecisionServiceTest(base.BaseTest): + def setUp(self): + base.BaseTest.setUp(self) + self.decision_service = self.optimizely.decision_service + # Set UserProfileService for the purposes of testing + self.decision_service.user_profile_service = user_profile.UserProfileService() + + def test_get_bucketing_id__no_bucketing_id_attribute(self): + """ Test that _get_bucketing_id returns correct bucketing ID when there is no bucketing ID attribute. """ + + # No attributes + self.assertEqual( + "test_user", self.decision_service._get_bucketing_id("test_user", None) + ) + + # With attributes, but no bucketing ID + self.assertEqual( + "test_user", + self.decision_service._get_bucketing_id( + "test_user", {"random_key": "random_value"} + ), + ) + + 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" + ) as mock_decision_service_logging: + self.assertEqual( + "user_bucket_value", + self.decision_service._get_bucketing_id( + "test_user", {"$opt_bucketing_id": "user_bucket_value"} + ), + ) + mock_decision_service_logging.debug.assert_not_called() + + 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" + ) as mock_decision_service_logging: + self.assertEqual( + "test_user", + self.decision_service._get_bucketing_id( + "test_user", {"$opt_bucketing_id": True} + ), + ) + mock_decision_service_logging.warning.assert_called_once_with( + "Bucketing ID attribute is not a string. Defaulted to user_id." + ) + mock_decision_service_logging.reset_mock() + + self.assertEqual( + "test_user", + self.decision_service._get_bucketing_id( + "test_user", {"$opt_bucketing_id": 5.9} + ), + ) + mock_decision_service_logging.warning.assert_called_once_with( + "Bucketing ID attribute is not a string. Defaulted to user_id." + ) + mock_decision_service_logging.reset_mock() + + self.assertEqual( + "test_user", + self.decision_service._get_bucketing_id( + "test_user", {"$opt_bucketing_id": 5} + ), + ) + mock_decision_service_logging.warning.assert_called_once_with( + "Bucketing ID attribute is not a string. Defaulted to user_id." + ) + + def test_set_forced_variation__invalid_experiment_key(self): + """ Test invalid experiment keys set fail to set a forced variation """ + + self.assertFalse( + self.decision_service.set_forced_variation( + self.project_config, + "test_experiment_not_in_datafile", + "test_user", + "variation", + ) + ) + self.assertFalse( + self.decision_service.set_forced_variation( + self.project_config, "", "test_user", "variation" + ) + ) + self.assertFalse( + self.decision_service.set_forced_variation( + self.project_config, None, "test_user", "variation" + ) + ) + + def test_set_forced_variation__invalid_variation_key(self): + """ Test invalid variation keys set fail to set a forced variation """ + + self.assertFalse( + self.decision_service.set_forced_variation( + self.project_config, + "test_experiment", + "test_user", + "variation_not_in_datafile", + ) + ) + self.assertTrue( + self.decision_service.set_forced_variation( + self.project_config, "test_experiment", "test_user", None + ) + ) + with mock.patch.object( + self.decision_service, "logger" + ) as mock_decision_service_logging: + self.assertIs( + self.decision_service.set_forced_variation( + self.project_config, "test_experiment", "test_user", "" + ), + False, + ) + mock_decision_service_logging.debug.assert_called_once_with( + "Variation key is invalid." + ) + + def test_set_forced_variation__multiple_sets(self): + """ Test multiple sets of experiments for one and multiple users work """ + + self.assertTrue( + self.decision_service.set_forced_variation( + self.project_config, "test_experiment", "test_user_1", "variation" + ) + ) + self.assertEqual( + self.decision_service.get_forced_variation( + self.project_config, "test_experiment", "test_user_1" + ).key, + "variation", + ) + # same user, same experiment, different variation + self.assertTrue( + self.decision_service.set_forced_variation( + self.project_config, "test_experiment", "test_user_1", "control" + ) + ) + self.assertEqual( + self.decision_service.get_forced_variation( + self.project_config, "test_experiment", "test_user_1" + ).key, + "control", + ) + # same user, different experiment + self.assertTrue( + self.decision_service.set_forced_variation( + self.project_config, "group_exp_1", "test_user_1", "group_exp_1_control" + ) + ) + self.assertEqual( + self.decision_service.get_forced_variation( + self.project_config, "group_exp_1", "test_user_1" + ).key, + "group_exp_1_control", + ) + + # different user + self.assertTrue( + self.decision_service.set_forced_variation( + self.project_config, "test_experiment", "test_user_2", "variation" + ) + ) + self.assertEqual( + self.decision_service.get_forced_variation( + self.project_config, "test_experiment", "test_user_2" + ).key, + "variation", + ) + # different user, different experiment + self.assertTrue( + self.decision_service.set_forced_variation( + self.project_config, "group_exp_1", "test_user_2", "group_exp_1_control" + ) + ) + self.assertEqual( + self.decision_service.get_forced_variation( + self.project_config, "group_exp_1", "test_user_2" + ).key, + "group_exp_1_control", + ) + + # make sure the first user forced variations are still valid + self.assertEqual( + self.decision_service.get_forced_variation( + self.project_config, "test_experiment", "test_user_1" + ).key, + "control", + ) + self.assertEqual( + self.decision_service.get_forced_variation( + self.project_config, "group_exp_1", "test_user_1" + ).key, + "group_exp_1_control", + ) - def setUp(self): - base.BaseTest.setUp(self) - self.decision_service = self.optimizely.decision_service - # Set UserProfileService for the purposes of testing - self.decision_service.user_profile_service = user_profile.UserProfileService() - - def test_get_bucketing_id__no_bucketing_id_attribute(self): - """ Test that _get_bucketing_id returns correct bucketing ID when there is no bucketing ID attribute. """ - - # No attributes - self.assertEqual('test_user', self.decision_service._get_bucketing_id('test_user', None)) - - # With attributes, but no bucketing ID - self.assertEqual('test_user', self.decision_service._get_bucketing_id('test_user', - {'random_key': 'random_value'})) - - 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') as mock_decision_service_logging: - self.assertEqual('user_bucket_value', - self.decision_service._get_bucketing_id('test_user', - {'$opt_bucketing_id': 'user_bucket_value'})) - mock_decision_service_logging.debug.assert_not_called() - - 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') as mock_decision_service_logging: - self.assertEqual('test_user', - self.decision_service._get_bucketing_id('test_user', - {'$opt_bucketing_id': True})) - mock_decision_service_logging.warning.assert_called_once_with( - 'Bucketing ID attribute is not a string. Defaulted to user_id.') - mock_decision_service_logging.reset_mock() - - self.assertEqual('test_user', - self.decision_service._get_bucketing_id('test_user', - {'$opt_bucketing_id': 5.9})) - mock_decision_service_logging.warning.assert_called_once_with( - 'Bucketing ID attribute is not a string. Defaulted to user_id.') - mock_decision_service_logging.reset_mock() - - self.assertEqual('test_user', - self.decision_service._get_bucketing_id('test_user', - {'$opt_bucketing_id': 5})) - mock_decision_service_logging.warning.assert_called_once_with( - 'Bucketing ID attribute is not a string. Defaulted to user_id.') - - def test_set_forced_variation__invalid_experiment_key(self): - """ Test invalid experiment keys set fail to set a forced variation """ - - self.assertFalse(self.decision_service.set_forced_variation( - self.project_config, - 'test_experiment_not_in_datafile', - 'test_user', - 'variation' - )) - self.assertFalse(self.decision_service.set_forced_variation(self.project_config, '', 'test_user', 'variation')) - self.assertFalse(self.decision_service.set_forced_variation(self.project_config, None, 'test_user', 'variation')) - - def test_set_forced_variation__invalid_variation_key(self): - """ Test invalid variation keys set fail to set a forced variation """ - - self.assertFalse(self.decision_service.set_forced_variation( - self.project_config, - 'test_experiment', 'test_user', - 'variation_not_in_datafile') - ) - self.assertTrue(self.decision_service.set_forced_variation( - self.project_config, - 'test_experiment', - 'test_user', - None) - ) - with mock.patch.object(self.decision_service, 'logger') as mock_decision_service_logging: - self.assertIs( - self.decision_service.set_forced_variation(self.project_config, 'test_experiment', 'test_user', ''), - False - ) - mock_decision_service_logging.debug.assert_called_once_with('Variation key is invalid.') - - def test_set_forced_variation__multiple_sets(self): - """ Test multiple sets of experiments for one and multiple users work """ - - self.assertTrue(self.decision_service.set_forced_variation( - self.project_config, - 'test_experiment', - 'test_user_1', - 'variation') - ) - self.assertEqual( - self.decision_service.get_forced_variation(self.project_config, 'test_experiment', 'test_user_1').key, - 'variation' - ) - # same user, same experiment, different variation - self.assertTrue( - self.decision_service.set_forced_variation(self.project_config, 'test_experiment', 'test_user_1', 'control') - ) - self.assertEqual( - self.decision_service.get_forced_variation(self.project_config, 'test_experiment', 'test_user_1').key, - 'control' - ) - # same user, different experiment - self.assertTrue( + def test_set_forced_variation_when_called_to_remove_forced_variation(self): + """ Test set_forced_variation when no variation is given. """ + # Test case where both user and experiment are present in the forced variation map + self.project_config.forced_variation_map = {} self.decision_service.set_forced_variation( - self.project_config, 'group_exp_1', 'test_user_1', 'group_exp_1_control' - ) - ) - self.assertEqual( - self.decision_service.get_forced_variation(self.project_config, 'group_exp_1', 'test_user_1').key, - 'group_exp_1_control' - ) - - # different user - self.assertTrue( - self.decision_service.set_forced_variation(self.project_config, 'test_experiment', 'test_user_2', 'variation') - ) - self.assertEqual( - self.decision_service.get_forced_variation(self.project_config, 'test_experiment', 'test_user_2').key, - 'variation' - ) - # different user, different experiment - self.assertTrue( + self.project_config, "test_experiment", "test_user", "variation" + ) + + with mock.patch.object( + self.decision_service, "logger" + ) as mock_decision_service_logging: + self.assertTrue( + self.decision_service.set_forced_variation( + self.project_config, "test_experiment", "test_user", None + ) + ) + mock_decision_service_logging.debug.assert_called_once_with( + 'Variation mapped to experiment "test_experiment" has been removed for user "test_user".' + ) + + # Test case where user is present in the forced variation map, but the given experiment isn't + self.project_config.forced_variation_map = {} self.decision_service.set_forced_variation( - self.project_config, 'group_exp_1', 'test_user_2', 'group_exp_1_control' - ) - ) - self.assertEqual( - self.decision_service.get_forced_variation(self.project_config, 'group_exp_1', 'test_user_2').key, - 'group_exp_1_control' - ) - - # make sure the first user forced variations are still valid - self.assertEqual( - self.decision_service.get_forced_variation(self.project_config, 'test_experiment', 'test_user_1').key, - 'control' - ) - self.assertEqual( - self.decision_service.get_forced_variation(self.project_config, 'group_exp_1', 'test_user_1').key, - 'group_exp_1_control' - ) - - def test_set_forced_variation_when_called_to_remove_forced_variation(self): - """ Test set_forced_variation when no variation is given. """ - # Test case where both user and experiment are present in the forced variation map - self.project_config.forced_variation_map = {} - self.decision_service.set_forced_variation(self.project_config, 'test_experiment', 'test_user', 'variation') - - with mock.patch.object(self.decision_service, 'logger') as mock_decision_service_logging: - self.assertTrue( - self.decision_service.set_forced_variation(self.project_config, 'test_experiment', 'test_user', None) - ) - mock_decision_service_logging.debug.assert_called_once_with( - 'Variation mapped to experiment "test_experiment" has been removed for user "test_user".' - ) - - # Test case where user is present in the forced variation map, but the given experiment isn't - self.project_config.forced_variation_map = {} - self.decision_service.set_forced_variation(self.project_config, 'test_experiment', 'test_user', 'variation') - - with mock.patch.object(self.decision_service, 'logger') as mock_decision_service_logging: - self.assertTrue(self.decision_service.set_forced_variation(self.project_config, 'group_exp_1', 'test_user', None)) - mock_decision_service_logging.debug.assert_called_once_with( - 'Nothing to remove. Variation mapped to experiment "group_exp_1" for user "test_user" does not exist.' - ) - - def test_get_forced_variation__invalid_user_id(self): - """ Test invalid user IDs return a null variation. """ - self.decision_service.forced_variation_map['test_user'] = {} - self.decision_service.forced_variation_map['test_user']['test_experiment'] = 'test_variation' - - self.assertIsNone(self.decision_service.get_forced_variation(self.project_config, 'test_experiment', None)) - self.assertIsNone(self.decision_service.get_forced_variation(self.project_config, 'test_experiment', '')) - - def test_get_forced_variation__invalid_experiment_key(self): - """ Test invalid experiment keys return a null variation. """ - self.decision_service.forced_variation_map['test_user'] = {} - self.decision_service.forced_variation_map['test_user']['test_experiment'] = 'test_variation' - - self.assertIsNone(self.decision_service.get_forced_variation( - self.project_config, 'test_experiment_not_in_datafile', 'test_user' - )) - self.assertIsNone(self.decision_service.get_forced_variation(self.project_config, None, 'test_user')) - self.assertIsNone(self.decision_service.get_forced_variation(self.project_config, '', 'test_user')) - - def test_get_forced_variation_with_none_set_for_user(self): - """ Test get_forced_variation when none set for user ID in forced variation map. """ - self.decision_service.forced_variation_map = {} - self.decision_service.forced_variation_map['test_user'] = {} - - with mock.patch.object(self.decision_service, 'logger') as mock_decision_service_logging: - self.assertIsNone(self.decision_service.get_forced_variation(self.project_config, 'test_experiment', 'test_user')) - mock_decision_service_logging.debug.assert_called_once_with( - 'No experiment "test_experiment" mapped to user "test_user" in the forced variation map.' - ) - - def test_get_forced_variation_missing_variation_mapped_to_experiment(self): - """ Test get_forced_variation when no variation found against given experiment for the user. """ - self.decision_service.forced_variation_map = {} - self.decision_service.forced_variation_map['test_user'] = {} - self.decision_service.forced_variation_map['test_user']['test_experiment'] = None - - with mock.patch.object(self.decision_service, 'logger') as mock_decision_service_logging: - self.assertIsNone(self.decision_service.get_forced_variation(self.project_config, 'test_experiment', 'test_user')) - - mock_decision_service_logging.debug.assert_called_once_with( - 'No variation mapped to experiment "test_experiment" in the forced variation map.' - ) - - def test_get_whitelisted_variation__user_in_forced_variation(self): - """ Test that expected variation is returned if user is forced in a variation. """ - - experiment = self.project_config.get_experiment_from_key('test_experiment') - with mock.patch.object(self.decision_service, 'logger') as mock_decision_service_logging: - self.assertEqual(entities.Variation('111128', 'control'), - self.decision_service.get_whitelisted_variation(self.project_config, experiment, 'user_1')) - - mock_decision_service_logging.info.assert_called_once_with( - 'User "user_1" is forced in variation "control".' - ) - - def test_get_whitelisted_variation__user_in_invalid_variation(self): - """ Test that get_whitelisted_variation returns None when variation user is whitelisted for is invalid. """ - - experiment = self.project_config.get_experiment_from_key('test_experiment') - with mock.patch('optimizely.project_config.ProjectConfig.get_variation_from_key', - return_value=None) as mock_get_variation_id: - self.assertIsNone(self.decision_service.get_whitelisted_variation(self.project_config, experiment, 'user_1')) - - mock_get_variation_id.assert_called_once_with('test_experiment', 'control') - - def test_get_stored_variation__stored_decision_available(self): - """ Test that stored decision is retrieved as expected. """ - - experiment = self.project_config.get_experiment_from_key('test_experiment') - profile = user_profile.UserProfile('test_user', experiment_bucket_map={'111127': {'variation_id': '111128'}}) - with mock.patch.object(self.decision_service, 'logger') as mock_decision_service_logging: - self.assertEqual(entities.Variation('111128', 'control'), - self.decision_service.get_stored_variation(self.project_config, experiment, profile)) - - mock_decision_service_logging.info.assert_called_once_with( - 'Found a stored decision. User "test_user" is in variation "control" of experiment "test_experiment".' - ) - - def test_get_stored_variation__no_stored_decision_available(self): - """ Test that get_stored_variation returns None when no decision is available. """ - - experiment = self.project_config.get_experiment_from_key('test_experiment') - profile = user_profile.UserProfile('test_user') - self.assertIsNone(self.decision_service.get_stored_variation(self.project_config, experiment, profile)) - - def test_get_variation__experiment_not_running(self): - """ Test that get_variation returns None if experiment is not Running. """ - - 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') as mock_get_forced_variation, \ - mock.patch.object(self.decision_service, 'logger') as mock_decision_service_logging, \ - mock.patch('optimizely.decision_service.DecisionService.get_stored_variation') as mock_get_stored_variation, \ - mock.patch('optimizely.helpers.audience.is_user_in_experiment') as mock_audience_check, \ - mock.patch('optimizely.bucketer.Bucketer.bucket') as mock_bucket, \ - mock.patch('optimizely.user_profile.UserProfileService.lookup') as mock_lookup, \ - mock.patch('optimizely.user_profile.UserProfileService.save') as mock_save: - self.assertIsNone(self.decision_service.get_variation(self.project_config, experiment, 'test_user', None)) - - mock_decision_service_logging.info.assert_called_once_with('Experiment "test_experiment" is not running.') - # Assert no calls are made to other services - self.assertEqual(0, mock_get_forced_variation.call_count) - self.assertEqual(0, mock_get_stored_variation.call_count) - self.assertEqual(0, mock_audience_check.call_count) - self.assertEqual(0, mock_bucket.call_count) - self.assertEqual(0, mock_lookup.call_count) - self.assertEqual(0, mock_save.call_count) - - def test_get_variation__bucketing_id_provided(self): - """ Test that get_variation calls bucket with correct bucketing ID if provided. """ - - experiment = self.project_config.get_experiment_from_key('test_experiment') - with mock.patch('optimizely.decision_service.DecisionService.get_forced_variation', return_value=None), \ - mock.patch('optimizely.decision_service.DecisionService.get_stored_variation', return_value=None), \ - mock.patch('optimizely.helpers.audience.is_user_in_experiment', return_value=True), \ - mock.patch('optimizely.bucketer.Bucketer.bucket') as mock_bucket: - self.decision_service.get_variation(self.project_config, - experiment, - 'test_user', - {'random_key': 'random_value', - '$opt_bucketing_id': 'user_bucket_value'}) - - # Assert that bucket is called with appropriate bucketing ID - mock_bucket.assert_called_once_with(self.project_config, experiment, 'test_user', 'user_bucket_value') - - def test_get_variation__user_whitelisted_for_variation(self): - """ Test that get_variation returns whitelisted variation if user is whitelisted. """ - - 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')) as mock_get_whitelisted_variation, \ - mock.patch('optimizely.decision_service.DecisionService.get_stored_variation') as mock_get_stored_variation, \ - mock.patch('optimizely.helpers.audience.is_user_in_experiment') as mock_audience_check, \ - mock.patch('optimizely.bucketer.Bucketer.bucket') as mock_bucket, \ - mock.patch('optimizely.user_profile.UserProfileService.lookup') as mock_lookup, \ - mock.patch('optimizely.user_profile.UserProfileService.save') as mock_save: - self.assertEqual(entities.Variation('111128', 'control'), - self.decision_service.get_variation(self.project_config, experiment, 'test_user', None)) - - # Assert that forced variation is returned and stored decision or bucketing service are not involved - mock_get_whitelisted_variation.assert_called_once_with(self.project_config, experiment, 'test_user') - self.assertEqual(0, mock_get_stored_variation.call_count) - self.assertEqual(0, mock_audience_check.call_count) - self.assertEqual(0, mock_bucket.call_count) - self.assertEqual(0, mock_lookup.call_count) - self.assertEqual(0, mock_save.call_count) - - def test_get_variation__user_has_stored_decision(self): - """ Test that get_variation returns stored decision if user has variation available for given experiment. """ - - experiment = self.project_config.get_experiment_from_key('test_experiment') - with mock.patch('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')) as mock_get_stored_variation, \ - mock.patch('optimizely.helpers.audience.is_user_in_experiment') as mock_audience_check, \ - mock.patch('optimizely.bucketer.Bucketer.bucket') as mock_bucket, \ - mock.patch( - 'optimizely.user_profile.UserProfileService.lookup', - return_value={'user_id': 'test_user', - 'experiment_bucket_map': {'111127': {'variation_id': '111128'}}}) as mock_lookup, \ - mock.patch('optimizely.user_profile.UserProfileService.save') as mock_save: - self.assertEqual(entities.Variation('111128', 'control'), - self.decision_service.get_variation(self.project_config, experiment, 'test_user', None)) - - # Assert that stored variation is returned and bucketing service is not involved - mock_get_whitelisted_variation.assert_called_once_with(self.project_config, experiment, 'test_user') - mock_lookup.assert_called_once_with('test_user') - mock_get_stored_variation.assert_called_once_with( - self.project_config, experiment, user_profile.UserProfile('test_user', {'111127': {'variation_id': '111128'}}) - ) - self.assertEqual(0, mock_audience_check.call_count) - self.assertEqual(0, mock_bucket.call_count) - self.assertEqual(0, mock_save.call_count) - - def test_get_variation__user_bucketed_for_new_experiment__user_profile_service_available(self): - """ Test that get_variation buckets and returns variation if no forced variation or decision available. + self.project_config, "test_experiment", "test_user", "variation" + ) + + with mock.patch.object( + self.decision_service, "logger" + ) as mock_decision_service_logging: + self.assertTrue( + self.decision_service.set_forced_variation( + self.project_config, "group_exp_1", "test_user", None + ) + ) + mock_decision_service_logging.debug.assert_called_once_with( + 'Nothing to remove. Variation mapped to experiment "group_exp_1" for user "test_user" does not exist.' + ) + + def test_get_forced_variation__invalid_user_id(self): + """ Test invalid user IDs return a null variation. """ + self.decision_service.forced_variation_map["test_user"] = {} + self.decision_service.forced_variation_map["test_user"][ + "test_experiment" + ] = "test_variation" + + self.assertIsNone( + self.decision_service.get_forced_variation( + self.project_config, "test_experiment", None + ) + ) + self.assertIsNone( + self.decision_service.get_forced_variation( + self.project_config, "test_experiment", "" + ) + ) + + def test_get_forced_variation__invalid_experiment_key(self): + """ Test invalid experiment keys return a null variation. """ + self.decision_service.forced_variation_map["test_user"] = {} + self.decision_service.forced_variation_map["test_user"][ + "test_experiment" + ] = "test_variation" + + self.assertIsNone( + self.decision_service.get_forced_variation( + self.project_config, "test_experiment_not_in_datafile", "test_user" + ) + ) + self.assertIsNone( + self.decision_service.get_forced_variation( + self.project_config, None, "test_user" + ) + ) + self.assertIsNone( + self.decision_service.get_forced_variation( + self.project_config, "", "test_user" + ) + ) + + def test_get_forced_variation_with_none_set_for_user(self): + """ Test get_forced_variation when none set for user ID in forced variation map. """ + self.decision_service.forced_variation_map = {} + self.decision_service.forced_variation_map["test_user"] = {} + + with mock.patch.object( + self.decision_service, "logger" + ) as mock_decision_service_logging: + self.assertIsNone( + self.decision_service.get_forced_variation( + self.project_config, "test_experiment", "test_user" + ) + ) + mock_decision_service_logging.debug.assert_called_once_with( + 'No experiment "test_experiment" mapped to user "test_user" in the forced variation map.' + ) + + def test_get_forced_variation_missing_variation_mapped_to_experiment(self): + """ Test get_forced_variation when no variation found against given experiment for the user. """ + self.decision_service.forced_variation_map = {} + self.decision_service.forced_variation_map["test_user"] = {} + self.decision_service.forced_variation_map["test_user"][ + "test_experiment" + ] = None + + with mock.patch.object( + self.decision_service, "logger" + ) as mock_decision_service_logging: + self.assertIsNone( + self.decision_service.get_forced_variation( + self.project_config, "test_experiment", "test_user" + ) + ) + + mock_decision_service_logging.debug.assert_called_once_with( + 'No variation mapped to experiment "test_experiment" in the forced variation map.' + ) + + def test_get_whitelisted_variation__user_in_forced_variation(self): + """ Test that expected variation is returned if user is forced in a variation. """ + + experiment = self.project_config.get_experiment_from_key("test_experiment") + with mock.patch.object( + self.decision_service, "logger" + ) as mock_decision_service_logging: + self.assertEqual( + entities.Variation("111128", "control"), + self.decision_service.get_whitelisted_variation( + self.project_config, experiment, "user_1" + ), + ) + + mock_decision_service_logging.info.assert_called_once_with( + 'User "user_1" is forced in variation "control".' + ) + + def test_get_whitelisted_variation__user_in_invalid_variation(self): + """ Test that get_whitelisted_variation returns None when variation user is whitelisted for is invalid. """ + + experiment = self.project_config.get_experiment_from_key("test_experiment") + with mock.patch( + "optimizely.project_config.ProjectConfig.get_variation_from_key", + return_value=None, + ) as mock_get_variation_id: + self.assertIsNone( + self.decision_service.get_whitelisted_variation( + self.project_config, experiment, "user_1" + ) + ) + + mock_get_variation_id.assert_called_once_with("test_experiment", "control") + + def test_get_stored_variation__stored_decision_available(self): + """ Test that stored decision is retrieved as expected. """ + + experiment = self.project_config.get_experiment_from_key("test_experiment") + profile = user_profile.UserProfile( + "test_user", experiment_bucket_map={"111127": {"variation_id": "111128"}} + ) + with mock.patch.object( + self.decision_service, "logger" + ) as mock_decision_service_logging: + self.assertEqual( + entities.Variation("111128", "control"), + self.decision_service.get_stored_variation( + self.project_config, experiment, profile + ), + ) + + mock_decision_service_logging.info.assert_called_once_with( + 'Found a stored decision. User "test_user" is in variation "control" of experiment "test_experiment".' + ) + + def test_get_stored_variation__no_stored_decision_available(self): + """ Test that get_stored_variation returns None when no decision is available. """ + + experiment = self.project_config.get_experiment_from_key("test_experiment") + profile = user_profile.UserProfile("test_user") + self.assertIsNone( + self.decision_service.get_stored_variation( + self.project_config, experiment, profile + ) + ) + + def test_get_variation__experiment_not_running(self): + """ Test that get_variation returns None if experiment is not Running. """ + + 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" + ) as mock_get_forced_variation, mock.patch.object( + self.decision_service, "logger" + ) as mock_decision_service_logging, mock.patch( + "optimizely.decision_service.DecisionService.get_stored_variation" + ) as mock_get_stored_variation, mock.patch( + "optimizely.helpers.audience.is_user_in_experiment" + ) as mock_audience_check, mock.patch( + "optimizely.bucketer.Bucketer.bucket" + ) as mock_bucket, mock.patch( + "optimizely.user_profile.UserProfileService.lookup" + ) as mock_lookup, mock.patch( + "optimizely.user_profile.UserProfileService.save" + ) as mock_save: + self.assertIsNone( + self.decision_service.get_variation( + self.project_config, experiment, "test_user", None + ) + ) + + mock_decision_service_logging.info.assert_called_once_with( + 'Experiment "test_experiment" is not running.' + ) + # Assert no calls are made to other services + self.assertEqual(0, mock_get_forced_variation.call_count) + self.assertEqual(0, mock_get_stored_variation.call_count) + self.assertEqual(0, mock_audience_check.call_count) + self.assertEqual(0, mock_bucket.call_count) + self.assertEqual(0, mock_lookup.call_count) + self.assertEqual(0, mock_save.call_count) + + def test_get_variation__bucketing_id_provided(self): + """ Test that get_variation calls bucket with correct bucketing ID if provided. """ + + experiment = self.project_config.get_experiment_from_key("test_experiment") + with mock.patch( + "optimizely.decision_service.DecisionService.get_forced_variation", + return_value=None, + ), mock.patch( + "optimizely.decision_service.DecisionService.get_stored_variation", + return_value=None, + ), mock.patch( + "optimizely.helpers.audience.is_user_in_experiment", return_value=True + ), mock.patch( + "optimizely.bucketer.Bucketer.bucket" + ) as mock_bucket: + self.decision_service.get_variation( + self.project_config, + experiment, + "test_user", + { + "random_key": "random_value", + "$opt_bucketing_id": "user_bucket_value", + }, + ) + + # Assert that bucket is called with appropriate bucketing ID + mock_bucket.assert_called_once_with( + self.project_config, experiment, "test_user", "user_bucket_value" + ) + + def test_get_variation__user_whitelisted_for_variation(self): + """ Test that get_variation returns whitelisted variation if user is whitelisted. """ + + 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"), + ) as mock_get_whitelisted_variation, mock.patch( + "optimizely.decision_service.DecisionService.get_stored_variation" + ) as mock_get_stored_variation, mock.patch( + "optimizely.helpers.audience.is_user_in_experiment" + ) as mock_audience_check, mock.patch( + "optimizely.bucketer.Bucketer.bucket" + ) as mock_bucket, mock.patch( + "optimizely.user_profile.UserProfileService.lookup" + ) as mock_lookup, mock.patch( + "optimizely.user_profile.UserProfileService.save" + ) as mock_save: + self.assertEqual( + entities.Variation("111128", "control"), + self.decision_service.get_variation( + self.project_config, experiment, "test_user", None + ), + ) + + # Assert that forced variation is returned and stored decision or bucketing service are not involved + mock_get_whitelisted_variation.assert_called_once_with( + self.project_config, experiment, "test_user" + ) + self.assertEqual(0, mock_get_stored_variation.call_count) + self.assertEqual(0, mock_audience_check.call_count) + self.assertEqual(0, mock_bucket.call_count) + self.assertEqual(0, mock_lookup.call_count) + self.assertEqual(0, mock_save.call_count) + + def test_get_variation__user_has_stored_decision(self): + """ Test that get_variation returns stored decision if user has variation available for given experiment. """ + + experiment = self.project_config.get_experiment_from_key("test_experiment") + with mock.patch( + "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"), + ) as mock_get_stored_variation, mock.patch( + "optimizely.helpers.audience.is_user_in_experiment" + ) as mock_audience_check, mock.patch( + "optimizely.bucketer.Bucketer.bucket" + ) as mock_bucket, mock.patch( + "optimizely.user_profile.UserProfileService.lookup", + return_value={ + "user_id": "test_user", + "experiment_bucket_map": {"111127": {"variation_id": "111128"}}, + }, + ) as mock_lookup, mock.patch( + "optimizely.user_profile.UserProfileService.save" + ) as mock_save: + self.assertEqual( + entities.Variation("111128", "control"), + self.decision_service.get_variation( + self.project_config, experiment, "test_user", None + ), + ) + + # Assert that stored variation is returned and bucketing service is not involved + mock_get_whitelisted_variation.assert_called_once_with( + self.project_config, experiment, "test_user" + ) + mock_lookup.assert_called_once_with("test_user") + mock_get_stored_variation.assert_called_once_with( + self.project_config, + experiment, + user_profile.UserProfile( + "test_user", {"111127": {"variation_id": "111128"}} + ), + ) + self.assertEqual(0, mock_audience_check.call_count) + self.assertEqual(0, mock_bucket.call_count) + self.assertEqual(0, mock_save.call_count) + + def test_get_variation__user_bucketed_for_new_experiment__user_profile_service_available( + 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. """ - experiment = self.project_config.get_experiment_from_key('test_experiment') - with mock.patch.object(self.decision_service, 'logger') as mock_decision_service_logging, \ - mock.patch('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=None) as mock_get_stored_variation, \ - mock.patch('optimizely.helpers.audience.is_user_in_experiment', return_value=True) as mock_audience_check, \ - mock.patch('optimizely.bucketer.Bucketer.bucket', - return_value=entities.Variation('111129', 'variation')) as mock_bucket, \ - mock.patch('optimizely.user_profile.UserProfileService.lookup', - return_value={'user_id': 'test_user', 'experiment_bucket_map': {}}) as mock_lookup, \ - mock.patch('optimizely.user_profile.UserProfileService.save') as mock_save: - self.assertEqual(entities.Variation('111129', 'variation'), - self.decision_service.get_variation(self.project_config, experiment, 'test_user', None)) - - # Assert that user is bucketed and new decision is stored - mock_get_whitelisted_variation.assert_called_once_with(self.project_config, experiment, 'test_user') - mock_lookup.assert_called_once_with('test_user') - self.assertEqual(1, mock_get_stored_variation.call_count) - mock_audience_check.assert_called_once_with(self.project_config, experiment, None, mock_decision_service_logging) - mock_bucket.assert_called_once_with(self.project_config, experiment, 'test_user', 'test_user') - mock_save.assert_called_once_with({'user_id': 'test_user', - 'experiment_bucket_map': {'111127': {'variation_id': '111129'}}}) - - def test_get_variation__user_bucketed_for_new_experiment__user_profile_service_not_available(self): - """ Test that get_variation buckets and returns variation if + experiment = self.project_config.get_experiment_from_key("test_experiment") + with mock.patch.object( + self.decision_service, "logger" + ) as mock_decision_service_logging, mock.patch( + "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=None, + ) as mock_get_stored_variation, mock.patch( + "optimizely.helpers.audience.is_user_in_experiment", return_value=True + ) as mock_audience_check, mock.patch( + "optimizely.bucketer.Bucketer.bucket", + return_value=entities.Variation("111129", "variation"), + ) as mock_bucket, mock.patch( + "optimizely.user_profile.UserProfileService.lookup", + return_value={"user_id": "test_user", "experiment_bucket_map": {}}, + ) as mock_lookup, mock.patch( + "optimizely.user_profile.UserProfileService.save" + ) as mock_save: + self.assertEqual( + entities.Variation("111129", "variation"), + self.decision_service.get_variation( + self.project_config, experiment, "test_user", None + ), + ) + + # Assert that user is bucketed and new decision is stored + mock_get_whitelisted_variation.assert_called_once_with( + self.project_config, experiment, "test_user" + ) + mock_lookup.assert_called_once_with("test_user") + self.assertEqual(1, mock_get_stored_variation.call_count) + mock_audience_check.assert_called_once_with( + self.project_config, experiment, None, mock_decision_service_logging + ) + mock_bucket.assert_called_once_with( + self.project_config, experiment, "test_user", "test_user" + ) + mock_save.assert_called_once_with( + { + "user_id": "test_user", + "experiment_bucket_map": {"111127": {"variation_id": "111129"}}, + } + ) + + def test_get_variation__user_bucketed_for_new_experiment__user_profile_service_not_available( + self, + ): + """ Test that get_variation buckets and returns variation if no forced variation and no user profile service available. """ - # Unset user profile service - self.decision_service.user_profile_service = None - - experiment = self.project_config.get_experiment_from_key('test_experiment') - with mock.patch.object(self.decision_service, 'logger') as mock_decision_service_logging,\ - mock.patch('optimizely.decision_service.DecisionService.get_whitelisted_variation', - return_value=None) as mock_get_whitelisted_variation, \ - mock.patch('optimizely.decision_service.DecisionService.get_stored_variation') as mock_get_stored_variation, \ - mock.patch('optimizely.helpers.audience.is_user_in_experiment', return_value=True) as mock_audience_check, \ - mock.patch('optimizely.bucketer.Bucketer.bucket', - return_value=entities.Variation('111129', 'variation')) as mock_bucket, \ - mock.patch('optimizely.user_profile.UserProfileService.lookup') as mock_lookup, \ - mock.patch('optimizely.user_profile.UserProfileService.save') as mock_save: - self.assertEqual(entities.Variation('111129', 'variation'), - self.decision_service.get_variation(self.project_config, experiment, 'test_user', None)) - - # Assert that user is bucketed and new decision is not stored as user profile service is not available - mock_get_whitelisted_variation.assert_called_once_with(self.project_config, experiment, 'test_user') - self.assertEqual(0, mock_lookup.call_count) - self.assertEqual(0, mock_get_stored_variation.call_count) - mock_audience_check.assert_called_once_with(self.project_config, experiment, None, mock_decision_service_logging) - mock_bucket.assert_called_once_with(self.project_config, experiment, 'test_user', 'test_user') - self.assertEqual(0, mock_save.call_count) - - def test_get_variation__user_does_not_meet_audience_conditions(self): - """ Test that get_variation returns None if user is not in experiment. """ - - experiment = self.project_config.get_experiment_from_key('test_experiment') - with mock.patch.object(self.decision_service, 'logger') as mock_decision_service_logging,\ - mock.patch('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=None) as mock_get_stored_variation, \ - mock.patch('optimizely.helpers.audience.is_user_in_experiment', return_value=False) as mock_audience_check, \ - mock.patch('optimizely.bucketer.Bucketer.bucket') as mock_bucket, \ - mock.patch('optimizely.user_profile.UserProfileService.lookup', - return_value={'user_id': 'test_user', 'experiment_bucket_map': {}}) as mock_lookup, \ - mock.patch('optimizely.user_profile.UserProfileService.save') as mock_save: - self.assertIsNone(self.decision_service.get_variation(self.project_config, experiment, 'test_user', None)) - - # Assert that user is bucketed and new decision is stored - mock_get_whitelisted_variation.assert_called_once_with(self.project_config, experiment, 'test_user') - mock_lookup.assert_called_once_with('test_user') - mock_get_stored_variation.assert_called_once_with( - self.project_config, - experiment, - user_profile.UserProfile('test_user') - ) - mock_audience_check.assert_called_once_with(self.project_config, experiment, None, mock_decision_service_logging) - self.assertEqual(0, mock_bucket.call_count) - self.assertEqual(0, mock_save.call_count) - - def test_get_variation__user_profile_in_invalid_format(self): - """ Test that get_variation handles invalid user profile gracefully. """ - - experiment = self.project_config.get_experiment_from_key('test_experiment') - with mock.patch.object(self.decision_service, 'logger') as mock_decision_service_logging,\ - mock.patch('optimizely.decision_service.DecisionService.get_whitelisted_variation', - return_value=None) as mock_get_whitelisted_variation, \ - mock.patch('optimizely.decision_service.DecisionService.get_stored_variation') as mock_get_stored_variation, \ - mock.patch('optimizely.helpers.audience.is_user_in_experiment', return_value=True) as mock_audience_check, \ - mock.patch('optimizely.bucketer.Bucketer.bucket', - return_value=entities.Variation('111129', 'variation')) as mock_bucket, \ - mock.patch('optimizely.user_profile.UserProfileService.lookup', - return_value='invalid_profile') as mock_lookup, \ - mock.patch('optimizely.user_profile.UserProfileService.save') as mock_save: - self.assertEqual(entities.Variation('111129', 'variation'), - self.decision_service.get_variation(self.project_config, experiment, 'test_user', None)) - - # Assert that user is bucketed and new decision is stored - mock_get_whitelisted_variation.assert_called_once_with(self.project_config, experiment, 'test_user') - mock_lookup.assert_called_once_with('test_user') - # Stored decision is not consulted as user profile is invalid - self.assertEqual(0, mock_get_stored_variation.call_count) - mock_audience_check.assert_called_once_with(self.project_config, experiment, None, mock_decision_service_logging) - mock_decision_service_logging.warning.assert_called_once_with('User profile has invalid format.') - mock_bucket.assert_called_once_with(self.project_config, experiment, 'test_user', 'test_user') - mock_save.assert_called_once_with({'user_id': 'test_user', - 'experiment_bucket_map': {'111127': {'variation_id': '111129'}}}) - - def test_get_variation__user_profile_lookup_fails(self): - """ Test that get_variation acts gracefully when lookup fails. """ - - experiment = self.project_config.get_experiment_from_key('test_experiment') - with mock.patch.object(self.decision_service, 'logger') as mock_decision_service_logging,\ - mock.patch('optimizely.decision_service.DecisionService.get_whitelisted_variation', - return_value=None) as mock_get_whitelisted_variation, \ - mock.patch('optimizely.decision_service.DecisionService.get_stored_variation') as mock_get_stored_variation, \ - mock.patch('optimizely.helpers.audience.is_user_in_experiment', return_value=True) as mock_audience_check, \ - mock.patch('optimizely.bucketer.Bucketer.bucket', - return_value=entities.Variation('111129', 'variation')) as mock_bucket, \ - mock.patch('optimizely.user_profile.UserProfileService.lookup', - side_effect=Exception('major problem')) as mock_lookup, \ - mock.patch('optimizely.user_profile.UserProfileService.save') as mock_save: - self.assertEqual(entities.Variation('111129', 'variation'), - self.decision_service.get_variation(self.project_config, experiment, 'test_user', None)) - - # Assert that user is bucketed and new decision is stored - mock_get_whitelisted_variation.assert_called_once_with(self.project_config, experiment, 'test_user') - mock_lookup.assert_called_once_with('test_user') - # Stored decision is not consulted as lookup failed - self.assertEqual(0, mock_get_stored_variation.call_count) - mock_audience_check.assert_called_once_with(self.project_config, experiment, None, mock_decision_service_logging) - mock_decision_service_logging.exception.assert_called_once_with( - 'Unable to retrieve user profile for user "test_user" as lookup failed.' - ) - mock_bucket.assert_called_once_with(self.project_config, experiment, 'test_user', 'test_user') - mock_save.assert_called_once_with({'user_id': 'test_user', - 'experiment_bucket_map': {'111127': {'variation_id': '111129'}}}) - - def test_get_variation__user_profile_save_fails(self): - """ Test that get_variation acts gracefully when save fails. """ - - experiment = self.project_config.get_experiment_from_key('test_experiment') - with mock.patch.object(self.decision_service, 'logger') as mock_decision_service_logging,\ - mock.patch('optimizely.decision_service.DecisionService.get_whitelisted_variation', - return_value=None) as mock_get_whitelisted_variation, \ - mock.patch('optimizely.decision_service.DecisionService.get_stored_variation') as mock_get_stored_variation, \ - mock.patch('optimizely.helpers.audience.is_user_in_experiment', return_value=True) as mock_audience_check, \ - mock.patch('optimizely.bucketer.Bucketer.bucket', - return_value=entities.Variation('111129', 'variation')) as mock_bucket, \ - mock.patch('optimizely.user_profile.UserProfileService.lookup', return_value=None) as mock_lookup, \ - mock.patch('optimizely.user_profile.UserProfileService.save', - side_effect=Exception('major problem')) as mock_save: - self.assertEqual(entities.Variation('111129', 'variation'), - self.decision_service.get_variation(self.project_config, experiment, 'test_user', None)) - - # Assert that user is bucketed and new decision is stored - mock_get_whitelisted_variation.assert_called_once_with(self.project_config, experiment, 'test_user') - mock_lookup.assert_called_once_with('test_user') - self.assertEqual(0, mock_get_stored_variation.call_count) - mock_audience_check.assert_called_once_with(self.project_config, experiment, None, mock_decision_service_logging) - mock_decision_service_logging.exception.assert_called_once_with( - 'Unable to save user profile for user "test_user".' - ) - mock_bucket.assert_called_once_with(self.project_config, experiment, 'test_user', 'test_user') - mock_save.assert_called_once_with({'user_id': 'test_user', - 'experiment_bucket_map': {'111127': {'variation_id': '111129'}}}) - - def test_get_variation__ignore_user_profile_when_specified(self): - """ Test that we ignore the user profile service if specified. """ - - experiment = self.project_config.get_experiment_from_key('test_experiment') - with mock.patch.object(self.decision_service, 'logger') as mock_decision_service_logging,\ - mock.patch('optimizely.decision_service.DecisionService.get_whitelisted_variation', - return_value=None) as mock_get_whitelisted_variation, \ - mock.patch('optimizely.helpers.audience.is_user_in_experiment', return_value=True) as mock_audience_check, \ - mock.patch('optimizely.bucketer.Bucketer.bucket', - return_value=entities.Variation('111129', 'variation')) as mock_bucket, \ - mock.patch('optimizely.user_profile.UserProfileService.lookup') as mock_lookup, \ - mock.patch('optimizely.user_profile.UserProfileService.save') as mock_save: - self.assertEqual( - entities.Variation('111129', 'variation'), - self.decision_service.get_variation( - self.project_config, experiment, 'test_user', None, ignore_user_profile=True - ) - ) - - # Assert that user is bucketed and new decision is NOT stored - mock_get_whitelisted_variation.assert_called_once_with(self.project_config, experiment, 'test_user') - mock_audience_check.assert_called_once_with(self.project_config, experiment, None, mock_decision_service_logging) - mock_bucket.assert_called_once_with(self.project_config, experiment, 'test_user', 'test_user') - self.assertEqual(0, mock_lookup.call_count) - self.assertEqual(0, mock_save.call_count) + # Unset user profile service + self.decision_service.user_profile_service = None + + experiment = self.project_config.get_experiment_from_key("test_experiment") + with mock.patch.object( + self.decision_service, "logger" + ) as mock_decision_service_logging, mock.patch( + "optimizely.decision_service.DecisionService.get_whitelisted_variation", + return_value=None, + ) as mock_get_whitelisted_variation, mock.patch( + "optimizely.decision_service.DecisionService.get_stored_variation" + ) as mock_get_stored_variation, mock.patch( + "optimizely.helpers.audience.is_user_in_experiment", return_value=True + ) as mock_audience_check, mock.patch( + "optimizely.bucketer.Bucketer.bucket", + return_value=entities.Variation("111129", "variation"), + ) as mock_bucket, mock.patch( + "optimizely.user_profile.UserProfileService.lookup" + ) as mock_lookup, mock.patch( + "optimizely.user_profile.UserProfileService.save" + ) as mock_save: + self.assertEqual( + entities.Variation("111129", "variation"), + self.decision_service.get_variation( + self.project_config, experiment, "test_user", None + ), + ) + + # Assert that user is bucketed and new decision is not stored as user profile service is not available + mock_get_whitelisted_variation.assert_called_once_with( + self.project_config, experiment, "test_user" + ) + self.assertEqual(0, mock_lookup.call_count) + self.assertEqual(0, mock_get_stored_variation.call_count) + mock_audience_check.assert_called_once_with( + self.project_config, experiment, None, mock_decision_service_logging + ) + mock_bucket.assert_called_once_with( + self.project_config, experiment, "test_user", "test_user" + ) + self.assertEqual(0, mock_save.call_count) + + def test_get_variation__user_does_not_meet_audience_conditions(self): + """ Test that get_variation returns None if user is not in experiment. """ + + experiment = self.project_config.get_experiment_from_key("test_experiment") + with mock.patch.object( + self.decision_service, "logger" + ) as mock_decision_service_logging, mock.patch( + "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=None, + ) as mock_get_stored_variation, mock.patch( + "optimizely.helpers.audience.is_user_in_experiment", return_value=False + ) as mock_audience_check, mock.patch( + "optimizely.bucketer.Bucketer.bucket" + ) as mock_bucket, mock.patch( + "optimizely.user_profile.UserProfileService.lookup", + return_value={"user_id": "test_user", "experiment_bucket_map": {}}, + ) as mock_lookup, mock.patch( + "optimizely.user_profile.UserProfileService.save" + ) as mock_save: + self.assertIsNone( + self.decision_service.get_variation( + self.project_config, experiment, "test_user", None + ) + ) + + # Assert that user is bucketed and new decision is stored + mock_get_whitelisted_variation.assert_called_once_with( + self.project_config, experiment, "test_user" + ) + mock_lookup.assert_called_once_with("test_user") + mock_get_stored_variation.assert_called_once_with( + self.project_config, experiment, user_profile.UserProfile("test_user") + ) + mock_audience_check.assert_called_once_with( + self.project_config, experiment, None, mock_decision_service_logging + ) + self.assertEqual(0, mock_bucket.call_count) + self.assertEqual(0, mock_save.call_count) + + def test_get_variation__user_profile_in_invalid_format(self): + """ Test that get_variation handles invalid user profile gracefully. """ + + experiment = self.project_config.get_experiment_from_key("test_experiment") + with mock.patch.object( + self.decision_service, "logger" + ) as mock_decision_service_logging, mock.patch( + "optimizely.decision_service.DecisionService.get_whitelisted_variation", + return_value=None, + ) as mock_get_whitelisted_variation, mock.patch( + "optimizely.decision_service.DecisionService.get_stored_variation" + ) as mock_get_stored_variation, mock.patch( + "optimizely.helpers.audience.is_user_in_experiment", return_value=True + ) as mock_audience_check, mock.patch( + "optimizely.bucketer.Bucketer.bucket", + return_value=entities.Variation("111129", "variation"), + ) as mock_bucket, mock.patch( + "optimizely.user_profile.UserProfileService.lookup", + return_value="invalid_profile", + ) as mock_lookup, mock.patch( + "optimizely.user_profile.UserProfileService.save" + ) as mock_save: + self.assertEqual( + entities.Variation("111129", "variation"), + self.decision_service.get_variation( + self.project_config, experiment, "test_user", None + ), + ) + + # Assert that user is bucketed and new decision is stored + mock_get_whitelisted_variation.assert_called_once_with( + self.project_config, experiment, "test_user" + ) + mock_lookup.assert_called_once_with("test_user") + # Stored decision is not consulted as user profile is invalid + self.assertEqual(0, mock_get_stored_variation.call_count) + mock_audience_check.assert_called_once_with( + self.project_config, experiment, None, mock_decision_service_logging + ) + mock_decision_service_logging.warning.assert_called_once_with( + "User profile has invalid format." + ) + mock_bucket.assert_called_once_with( + self.project_config, experiment, "test_user", "test_user" + ) + mock_save.assert_called_once_with( + { + "user_id": "test_user", + "experiment_bucket_map": {"111127": {"variation_id": "111129"}}, + } + ) + def test_get_variation__user_profile_lookup_fails(self): + """ Test that get_variation acts gracefully when lookup fails. """ + + experiment = self.project_config.get_experiment_from_key("test_experiment") + with mock.patch.object( + self.decision_service, "logger" + ) as mock_decision_service_logging, mock.patch( + "optimizely.decision_service.DecisionService.get_whitelisted_variation", + return_value=None, + ) as mock_get_whitelisted_variation, mock.patch( + "optimizely.decision_service.DecisionService.get_stored_variation" + ) as mock_get_stored_variation, mock.patch( + "optimizely.helpers.audience.is_user_in_experiment", return_value=True + ) as mock_audience_check, mock.patch( + "optimizely.bucketer.Bucketer.bucket", + return_value=entities.Variation("111129", "variation"), + ) as mock_bucket, mock.patch( + "optimizely.user_profile.UserProfileService.lookup", + side_effect=Exception("major problem"), + ) as mock_lookup, mock.patch( + "optimizely.user_profile.UserProfileService.save" + ) as mock_save: + self.assertEqual( + entities.Variation("111129", "variation"), + self.decision_service.get_variation( + self.project_config, experiment, "test_user", None + ), + ) + + # Assert that user is bucketed and new decision is stored + mock_get_whitelisted_variation.assert_called_once_with( + self.project_config, experiment, "test_user" + ) + mock_lookup.assert_called_once_with("test_user") + # Stored decision is not consulted as lookup failed + self.assertEqual(0, mock_get_stored_variation.call_count) + mock_audience_check.assert_called_once_with( + self.project_config, experiment, None, mock_decision_service_logging + ) + mock_decision_service_logging.exception.assert_called_once_with( + 'Unable to retrieve user profile for user "test_user" as lookup failed.' + ) + mock_bucket.assert_called_once_with( + self.project_config, experiment, "test_user", "test_user" + ) + mock_save.assert_called_once_with( + { + "user_id": "test_user", + "experiment_bucket_map": {"111127": {"variation_id": "111129"}}, + } + ) -class FeatureFlagDecisionTests(base.BaseTest): + def test_get_variation__user_profile_save_fails(self): + """ Test that get_variation acts gracefully when save fails. """ + + experiment = self.project_config.get_experiment_from_key("test_experiment") + with mock.patch.object( + self.decision_service, "logger" + ) as mock_decision_service_logging, mock.patch( + "optimizely.decision_service.DecisionService.get_whitelisted_variation", + return_value=None, + ) as mock_get_whitelisted_variation, mock.patch( + "optimizely.decision_service.DecisionService.get_stored_variation" + ) as mock_get_stored_variation, mock.patch( + "optimizely.helpers.audience.is_user_in_experiment", return_value=True + ) as mock_audience_check, mock.patch( + "optimizely.bucketer.Bucketer.bucket", + return_value=entities.Variation("111129", "variation"), + ) as mock_bucket, mock.patch( + "optimizely.user_profile.UserProfileService.lookup", return_value=None + ) as mock_lookup, mock.patch( + "optimizely.user_profile.UserProfileService.save", + side_effect=Exception("major problem"), + ) as mock_save: + self.assertEqual( + entities.Variation("111129", "variation"), + self.decision_service.get_variation( + self.project_config, experiment, "test_user", None + ), + ) + + # Assert that user is bucketed and new decision is stored + mock_get_whitelisted_variation.assert_called_once_with( + self.project_config, experiment, "test_user" + ) + mock_lookup.assert_called_once_with("test_user") + self.assertEqual(0, mock_get_stored_variation.call_count) + mock_audience_check.assert_called_once_with( + self.project_config, experiment, None, mock_decision_service_logging + ) + mock_decision_service_logging.exception.assert_called_once_with( + 'Unable to save user profile for user "test_user".' + ) + mock_bucket.assert_called_once_with( + self.project_config, experiment, "test_user", "test_user" + ) + mock_save.assert_called_once_with( + { + "user_id": "test_user", + "experiment_bucket_map": {"111127": {"variation_id": "111129"}}, + } + ) - def setUp(self): - base.BaseTest.setUp(self) - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - self.project_config = opt_obj.config_manager.get_config() - self.decision_service = opt_obj.decision_service - self.mock_decision_logger = mock.patch.object(self.decision_service, 'logger') - 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). """ - - with self.mock_config_logger as mock_logging: - no_experiment_rollout = self.project_config.get_rollout_from_id('201111') - self.assertEqual( - decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), - self.decision_service.get_variation_for_rollout(self.project_config, no_experiment_rollout, 'test_user') - ) - - # Assert no log messages were generated - self.assertEqual(0, mock_logging.call_count) - - def test_get_variation_for_rollout__returns_decision_if_user_in_rollout(self): - """ Test that get_variation_for_rollout returns Decision with experiment/variation + def test_get_variation__ignore_user_profile_when_specified(self): + """ Test that we ignore the user profile service if specified. """ + + experiment = self.project_config.get_experiment_from_key("test_experiment") + with mock.patch.object( + self.decision_service, "logger" + ) as mock_decision_service_logging, mock.patch( + "optimizely.decision_service.DecisionService.get_whitelisted_variation", + return_value=None, + ) as mock_get_whitelisted_variation, mock.patch( + "optimizely.helpers.audience.is_user_in_experiment", return_value=True + ) as mock_audience_check, mock.patch( + "optimizely.bucketer.Bucketer.bucket", + return_value=entities.Variation("111129", "variation"), + ) as mock_bucket, mock.patch( + "optimizely.user_profile.UserProfileService.lookup" + ) as mock_lookup, mock.patch( + "optimizely.user_profile.UserProfileService.save" + ) as mock_save: + self.assertEqual( + entities.Variation("111129", "variation"), + self.decision_service.get_variation( + self.project_config, + experiment, + "test_user", + None, + ignore_user_profile=True, + ), + ) + + # Assert that user is bucketed and new decision is NOT stored + mock_get_whitelisted_variation.assert_called_once_with( + self.project_config, experiment, "test_user" + ) + mock_audience_check.assert_called_once_with( + self.project_config, experiment, None, mock_decision_service_logging + ) + mock_bucket.assert_called_once_with( + self.project_config, experiment, "test_user", "test_user" + ) + self.assertEqual(0, mock_lookup.call_count) + self.assertEqual(0, mock_save.call_count) + + +class FeatureFlagDecisionTests(base.BaseTest): + def setUp(self): + base.BaseTest.setUp(self) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + self.project_config = opt_obj.config_manager.get_config() + self.decision_service = opt_obj.decision_service + self.mock_decision_logger = mock.patch.object(self.decision_service, "logger") + 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). """ + + with self.mock_config_logger as mock_logging: + no_experiment_rollout = self.project_config.get_rollout_from_id("201111") + self.assertEqual( + decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + self.decision_service.get_variation_for_rollout( + self.project_config, no_experiment_rollout, "test_user" + ), + ) + + # Assert no log messages were generated + self.assertEqual(0, mock_logging.call_count) + + 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') - - with mock.patch('optimizely.helpers.audience.is_user_in_experiment', 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: - self.assertEqual(decision_service.Decision(self.project_config.get_experiment_from_id('211127'), - self.project_config.get_variation_from_id('211127', '211129'), - enums.DecisionSources.ROLLOUT), - self.decision_service.get_variation_for_rollout(self.project_config, rollout, 'test_user')) - - # Check all log messages - mock_decision_service_logging.debug.assert_has_calls([ - mock.call('User "test_user" meets conditions for targeting rule 1.'), - mock.call('User "test_user" is in variation 211129 of experiment 211127.'), - ]) - - # 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' - ) - - 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') - - with mock.patch('optimizely.helpers.audience.is_user_in_experiment', 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: - self.assertEqual(decision_service.Decision(self.project_config.get_experiment_from_id('211127'), - self.project_config.get_variation_from_id('211127', '211129'), - enums.DecisionSources.ROLLOUT), - self.decision_service.get_variation_for_rollout(self.project_config, - rollout, - 'test_user', - {'$opt_bucketing_id': 'user_bucket_value'})) - - # Check all log messages - mock_decision_service_logging.debug.assert_has_calls([ - mock.call('User "test_user" meets conditions for targeting rule 1.'), - mock.call('User "test_user" is in variation 211129 of experiment 211127.') - ]) - # 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', - '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") + + with mock.patch( + "optimizely.helpers.audience.is_user_in_experiment", 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: + self.assertEqual( + decision_service.Decision( + self.project_config.get_experiment_from_id("211127"), + self.project_config.get_variation_from_id("211127", "211129"), + enums.DecisionSources.ROLLOUT, + ), + self.decision_service.get_variation_for_rollout( + self.project_config, rollout, "test_user" + ), + ) + + # Check all log messages + mock_decision_service_logging.debug.assert_has_calls( + [ + mock.call('User "test_user" meets conditions for targeting rule 1.'), + mock.call( + 'User "test_user" is in variation 211129 of experiment 211127.' + ), + ] + ) - rollout = self.project_config.get_rollout_from_id('211111') - 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.is_user_in_experiment', 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]): - self.assertEqual( - decision_service.Decision(everyone_else_exp, variation_to_mock, enums.DecisionSources.ROLLOUT), - self.decision_service.get_variation_for_rollout(self.project_config, rollout, 'test_user')) - - # Check that after first experiment, it skips to the last experiment to check - self.assertEqual( - [mock.call( - self.project_config, self.project_config.get_experiment_from_key('211127'), None, mock_decision_service_logging - ), - mock.call( - self.project_config, - self.project_config.get_experiment_from_key('211147'), - None, - mock_decision_service_logging - ) - ], - mock_audience_check.call_args_list - ) - - # Check all log messages - mock_decision_service_logging.debug.assert_has_calls([ - mock.call('User "test_user" meets conditions for targeting rule 1.'), - mock.call('User "test_user" is not in the traffic group for the targeting else. ' - 'Checking "Everyone Else" rule now.'), - mock.call('User "test_user" meets conditions for 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') - - with mock.patch('optimizely.helpers.audience.is_user_in_experiment', return_value=False) as mock_audience_check, \ - self.mock_decision_logger as mock_decision_service_logging: - self.assertEqual(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), - self.decision_service.get_variation_for_rollout(self.project_config, rollout, 'test_user')) - - # Check that all experiments in rollout layer were checked - self.assertEqual( - [mock.call( - self.project_config, self.project_config.get_experiment_from_key('211127'), None, mock_decision_service_logging - ), - mock.call( - self.project_config, self.project_config.get_experiment_from_key('211137'), None, mock_decision_service_logging - ), - mock.call( - self.project_config, self.project_config.get_experiment_from_key('211147'), None, mock_decision_service_logging - )], - mock_audience_check.call_args_list - ) - - # Check all log messages - mock_decision_service_logging.debug.assert_has_calls([ - mock.call('User "test_user" does not meet conditions for targeting rule 1.'), - mock.call('User "test_user" does not meet conditions for targeting rule 2.') - ]) - - def test_get_variation_for_feature__returns_variation_for_feature_in_experiment(self): - """ Test that get_variation_for_feature returns the variation of the experiment the feature is associated with. """ - - feature = self.project_config.get_feature_from_key('test_feature_in_experiment') - - expected_experiment = self.project_config.get_experiment_from_key('test_experiment') - expected_variation = self.project_config.get_variation_from_id('test_experiment', '111129') - decision_patch = mock.patch( - 'optimizely.decision_service.DecisionService.get_variation', - return_value=expected_variation - ) - with decision_patch as mock_decision, self.mock_decision_logger as mock_decision_service_logging: - self.assertEqual(decision_service.Decision(expected_experiment, - expected_variation, - enums.DecisionSources.FEATURE_TEST), - self.decision_service.get_variation_for_feature(self.project_config, feature, 'test_user')) - - mock_decision.assert_called_once_with( - self.project_config, self.project_config.get_experiment_from_key('test_experiment'), 'test_user', None - ) - - # Check log message - mock_decision_service_logging.debug.assert_called_once_with( - 'User "test_user" is in variation variation of experiment test_experiment.' - ) - - 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. """ + # 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", + ) - feature = self.project_config.get_feature_from_key('test_feature_in_rollout') + 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") + + with mock.patch( + "optimizely.helpers.audience.is_user_in_experiment", 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: + self.assertEqual( + decision_service.Decision( + self.project_config.get_experiment_from_id("211127"), + self.project_config.get_variation_from_id("211127", "211129"), + enums.DecisionSources.ROLLOUT, + ), + self.decision_service.get_variation_for_rollout( + self.project_config, + rollout, + "test_user", + {"$opt_bucketing_id": "user_bucket_value"}, + ), + ) + + # Check all log messages + mock_decision_service_logging.debug.assert_has_calls( + [ + mock.call('User "test_user" meets conditions for targeting rule 1.'), + mock.call( + 'User "test_user" is in variation 211129 of experiment 211127.' + ), + ] + ) + # 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", + "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. """ - expected_variation = self.project_config.get_variation_from_id('211127', '211129') - get_variation_for_rollout_patch = mock.patch( - 'optimizely.decision_service.DecisionService.get_variation_for_rollout', - return_value=expected_variation - ) - with get_variation_for_rollout_patch as mock_get_variation_for_rollout, \ - self.mock_decision_logger as mock_decision_service_logging: - self.assertEqual(expected_variation, self.decision_service.get_variation_for_feature( - self.project_config, feature, 'test_user' - )) + rollout = self.project_config.get_rollout_from_id("211111") + everyone_else_exp = self.project_config.get_experiment_from_id("211147") + variation_to_mock = self.project_config.get_variation_from_id( + "211147", "211149" + ) - 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) + with mock.patch( + "optimizely.helpers.audience.is_user_in_experiment", 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] + ): + self.assertEqual( + decision_service.Decision( + everyone_else_exp, variation_to_mock, enums.DecisionSources.ROLLOUT + ), + self.decision_service.get_variation_for_rollout( + self.project_config, rollout, "test_user" + ), + ) + + # Check that after first experiment, it skips to the last experiment to check + self.assertEqual( + [ + mock.call( + self.project_config, + self.project_config.get_experiment_from_key("211127"), + None, + mock_decision_service_logging, + ), + mock.call( + self.project_config, + self.project_config.get_experiment_from_key("211147"), + None, + mock_decision_service_logging, + ), + ], + mock_audience_check.call_args_list, + ) - # Assert no log messages were generated - self.assertEqual(0, mock_decision_service_logging.debug.call_count) - self.assertEqual(0, len(mock_decision_service_logging.method_calls)) + # Check all log messages + mock_decision_service_logging.debug.assert_has_calls( + [ + mock.call('User "test_user" meets conditions for targeting rule 1.'), + mock.call( + 'User "test_user" is not in the traffic group for the targeting else. ' + 'Checking "Everyone Else" rule now.' + ), + mock.call( + 'User "test_user" meets conditions for targeting rule "Everyone Else".' + ), + ] + ) - def test_get_variation_for_feature__returns_variation_if_user_not_in_experiment_but_in_rollout(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. """ + 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") + + with mock.patch( + "optimizely.helpers.audience.is_user_in_experiment", return_value=False + ) as mock_audience_check, self.mock_decision_logger as mock_decision_service_logging: + self.assertEqual( + decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + self.decision_service.get_variation_for_rollout( + self.project_config, rollout, "test_user" + ), + ) + + # Check that all experiments in rollout layer were checked + self.assertEqual( + [ + mock.call( + self.project_config, + self.project_config.get_experiment_from_key("211127"), + None, + mock_decision_service_logging, + ), + mock.call( + self.project_config, + self.project_config.get_experiment_from_key("211137"), + None, + mock_decision_service_logging, + ), + mock.call( + self.project_config, + self.project_config.get_experiment_from_key("211147"), + None, + mock_decision_service_logging, + ), + ], + mock_audience_check.call_args_list, + ) - feature = self.project_config.get_feature_from_key('test_feature_in_experiment_and_rollout') - - expected_experiment = self.project_config.get_experiment_from_key('211127') - expected_variation = self.project_config.get_variation_from_id('211127', '211129') - with mock.patch( - 'optimizely.helpers.audience.is_user_in_experiment', - 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): - self.assertEqual(decision_service.Decision(expected_experiment, - expected_variation, - enums.DecisionSources.ROLLOUT), - self.decision_service.get_variation_for_feature(self.project_config, feature, 'test_user')) - - self.assertEqual(2, mock_audience_check.call_count) - mock_audience_check.assert_any_call(self.project_config, - self.project_config.get_experiment_from_key('group_exp_2'), None, - mock_decision_service_logging) - mock_audience_check.assert_any_call(self.project_config, - self.project_config.get_experiment_from_key('211127'), None, - mock_decision_service_logging) - - 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. """ + # Check all log messages + mock_decision_service_logging.debug.assert_has_calls( + [ + mock.call( + 'User "test_user" does not meet conditions for targeting rule 1.' + ), + mock.call( + 'User "test_user" does not meet conditions for targeting rule 2.' + ), + ] + ) - feature = self.project_config.get_feature_from_key('test_feature_in_group') - - expected_experiment = self.project_config.get_experiment_from_key('group_exp_1') - expected_variation = self.project_config.get_variation_from_id('group_exp_1', '28901') - with mock.patch( - 'optimizely.decision_service.DecisionService.get_experiment_in_group', - return_value=self.project_config.get_experiment_from_key('group_exp_1')) as mock_get_experiment_in_group, \ - mock.patch('optimizely.decision_service.DecisionService.get_variation', - return_value=expected_variation) as mock_decision: - self.assertEqual(decision_service.Decision(expected_experiment, - expected_variation, - enums.DecisionSources.FEATURE_TEST), - self.decision_service.get_variation_for_feature(self.project_config, feature, 'test_user')) - - mock_get_experiment_in_group.assert_called_once_with( - self.project_config, self.project_config.get_group('19228'), 'test_user' - ) - mock_decision.assert_called_once_with( - self.project_config, self.project_config.get_experiment_from_key('group_exp_1'), 'test_user', None - ) - - def test_get_variation_for_feature__returns_none_for_user_not_in_group(self): - """ Test that get_variation_for_feature returns None for - user not in group and the feature is not part of a rollout. """ + def test_get_variation_for_feature__returns_variation_for_feature_in_experiment( + self, + ): + """ Test that get_variation_for_feature returns the variation + of the experiment the feature is associated with. """ - feature = self.project_config.get_feature_from_key('test_feature_in_group') + feature = self.project_config.get_feature_from_key("test_feature_in_experiment") - with mock.patch('optimizely.decision_service.DecisionService.get_experiment_in_group', - return_value=None) as mock_get_experiment_in_group, \ - mock.patch('optimizely.decision_service.DecisionService.get_variation') as mock_decision: - self.assertEqual(decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), - self.decision_service.get_variation_for_feature(self.project_config, feature, 'test_user')) + expected_experiment = self.project_config.get_experiment_from_key( + "test_experiment" + ) + expected_variation = self.project_config.get_variation_from_id( + "test_experiment", "111129" + ) + decision_patch = mock.patch( + "optimizely.decision_service.DecisionService.get_variation", + return_value=expected_variation, + ) + with decision_patch as mock_decision, self.mock_decision_logger as mock_decision_service_logging: + self.assertEqual( + decision_service.Decision( + expected_experiment, + expected_variation, + enums.DecisionSources.FEATURE_TEST, + ), + self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user" + ), + ) + + mock_decision.assert_called_once_with( + self.project_config, + self.project_config.get_experiment_from_key("test_experiment"), + "test_user", + None, + ) - mock_get_experiment_in_group.assert_called_once_with( - self.project_config, self.project_config.get_group('19228'), 'test_user' - ) - self.assertFalse(mock_decision.called) + # Check log message + mock_decision_service_logging.debug.assert_called_once_with( + 'User "test_user" is in variation variation of experiment test_experiment.' + ) - 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. """ + 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. """ - feature = self.project_config.get_feature_from_key('test_feature_in_experiment') + feature = self.project_config.get_feature_from_key("test_feature_in_rollout") - with mock.patch('optimizely.decision_service.DecisionService.get_variation', return_value=None) as mock_decision: - self.assertEqual(decision_service.Decision(None, - None, - enums.DecisionSources.ROLLOUT), - self.decision_service.get_variation_for_feature(self.project_config, feature, 'test_user')) + expected_variation = self.project_config.get_variation_from_id( + "211127", "211129" + ) + get_variation_for_rollout_patch = mock.patch( + "optimizely.decision_service.DecisionService.get_variation_for_rollout", + return_value=expected_variation, + ) + with \ + get_variation_for_rollout_patch as mock_get_variation_for_rollout, \ + self.mock_decision_logger as mock_decision_service_logging: + self.assertEqual( + expected_variation, + self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user" + ), + ) + + 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 + ) - mock_decision.assert_called_once_with( - self.project_config, self.project_config.get_experiment_from_key('test_experiment'), 'test_user', None - ) + # Assert no log messages were generated + self.assertEqual(0, mock_decision_service_logging.debug.call_count) + self.assertEqual(0, len(mock_decision_service_logging.method_calls)) - def test_get_variation_for_feature__returns_none_for_invalid_group_id(self): - """ Test that get_variation_for_feature returns None for unknown group ID. """ + def test_get_variation_for_feature__returns_variation_if_user_not_in_experiment_but_in_rollout( + 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. """ - feature = self.project_config.get_feature_from_key('test_feature_in_group') - feature.groupId = 'aabbccdd' + feature = self.project_config.get_feature_from_key( + "test_feature_in_experiment_and_rollout" + ) - with self.mock_decision_logger as mock_decision_service_logging: - self.assertEqual( - decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), - self.decision_service.get_variation_for_feature(self.project_config, feature, 'test_user') - ) - mock_decision_service_logging.error.assert_called_once_with( - enums.Errors.INVALID_GROUP_ID.format('_get_variation_for_feature') - ) + expected_experiment = self.project_config.get_experiment_from_key("211127") + expected_variation = self.project_config.get_variation_from_id( + "211127", "211129" + ) + with mock.patch( + "optimizely.helpers.audience.is_user_in_experiment", + 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 + ): + self.assertEqual( + decision_service.Decision( + expected_experiment, + expected_variation, + enums.DecisionSources.ROLLOUT, + ), + self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user" + ), + ) + + self.assertEqual(2, mock_audience_check.call_count) + mock_audience_check.assert_any_call( + self.project_config, + self.project_config.get_experiment_from_key("group_exp_2"), + None, + mock_decision_service_logging, + ) + mock_audience_check.assert_any_call( + self.project_config, + self.project_config.get_experiment_from_key("211127"), + None, + mock_decision_service_logging, + ) - def test_get_variation_for_feature__returns_none_for_user_in_group_experiment_not_associated_with_feature(self): - """ Test that if a user is in the mutex group but the experiment is - not targeting a feature, then None is returned. """ + 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. """ - feature = self.project_config.get_feature_from_key('test_feature_in_group') + feature = self.project_config.get_feature_from_key("test_feature_in_group") - with mock.patch('optimizely.decision_service.DecisionService.get_experiment_in_group', - return_value=self.project_config.get_experiment_from_key('group_exp_2')) as mock_decision: - self.assertEqual(decision_service.Decision(None, - None, - enums.DecisionSources.ROLLOUT), - self.decision_service.get_variation_for_feature(self.project_config, feature, 'test_user')) + expected_experiment = self.project_config.get_experiment_from_key("group_exp_1") + expected_variation = self.project_config.get_variation_from_id( + "group_exp_1", "28901" + ) + with mock.patch( + "optimizely.decision_service.DecisionService.get_experiment_in_group", + return_value=self.project_config.get_experiment_from_key("group_exp_1"), + ) as mock_get_experiment_in_group, mock.patch( + "optimizely.decision_service.DecisionService.get_variation", + return_value=expected_variation, + ) as mock_decision: + self.assertEqual( + decision_service.Decision( + expected_experiment, + expected_variation, + enums.DecisionSources.FEATURE_TEST, + ), + self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user" + ), + ) + + mock_get_experiment_in_group.assert_called_once_with( + self.project_config, self.project_config.get_group("19228"), "test_user" + ) + mock_decision.assert_called_once_with( + self.project_config, + self.project_config.get_experiment_from_key("group_exp_1"), + "test_user", + None, + ) - mock_decision.assert_called_once_with(self.project_config, self.project_config.get_group('19228'), 'test_user') + def test_get_variation_for_feature__returns_none_for_user_not_in_group(self): + """ Test that get_variation_for_feature returns None for + user not in group and the feature is not part of a rollout. """ - def test_get_experiment_in_group(self): - """ Test that get_experiment_in_group returns the bucketed experiment for the user. """ + feature = self.project_config.get_feature_from_key("test_feature_in_group") + + with mock.patch( + "optimizely.decision_service.DecisionService.get_experiment_in_group", + return_value=None, + ) as mock_get_experiment_in_group, mock.patch( + "optimizely.decision_service.DecisionService.get_variation" + ) as mock_decision: + self.assertEqual( + decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user" + ), + ) + + mock_get_experiment_in_group.assert_called_once_with( + self.project_config, self.project_config.get_group("19228"), "test_user" + ) + self.assertFalse(mock_decision.called) + + def test_get_variation_for_feature__returns_none_for_user_not_in_experiment(self): + """ Test that get_variation_for_feature returns None for user not in the associated experiment. """ + + feature = self.project_config.get_feature_from_key("test_feature_in_experiment") + + with mock.patch( + "optimizely.decision_service.DecisionService.get_variation", + return_value=None, + ) as mock_decision: + self.assertEqual( + decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user" + ), + ) + + mock_decision.assert_called_once_with( + self.project_config, + self.project_config.get_experiment_from_key("test_experiment"), + "test_user", + None, + ) - group = self.project_config.get_group('19228') - experiment = self.project_config.get_experiment_from_id('32222') - with mock.patch('optimizely.bucketer.Bucketer.find_bucket', return_value='32222'), \ - self.mock_decision_logger as mock_decision_service_logging: - self.assertEqual(experiment, self.decision_service.get_experiment_in_group( - self.project_config, group, 'test_user' - )) + def test_get_variation_for_feature__returns_none_for_invalid_group_id(self): + """ Test that get_variation_for_feature returns None for unknown group ID. """ + + feature = self.project_config.get_feature_from_key("test_feature_in_group") + feature.groupId = "aabbccdd" + + with self.mock_decision_logger as mock_decision_service_logging: + self.assertEqual( + decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user" + ), + ) + mock_decision_service_logging.error.assert_called_once_with( + enums.Errors.INVALID_GROUP_ID.format("_get_variation_for_feature") + ) - mock_decision_service_logging.info.assert_called_once_with( - 'User with bucketing ID "test_user" is in experiment group_exp_1 of group 19228.' - ) + def test_get_variation_for_feature__returns_none_for_user_in_group_experiment_not_associated_with_feature( + self, + ): + """ Test that if a user is in the mutex group but the experiment is + not targeting a feature, then None is returned. """ - def test_get_experiment_in_group__returns_none_if_user_not_in_group(self): - """ Test that get_experiment_in_group returns None if the user is not bucketed into the group. """ + feature = self.project_config.get_feature_from_key("test_feature_in_group") + + with mock.patch( + "optimizely.decision_service.DecisionService.get_experiment_in_group", + return_value=self.project_config.get_experiment_from_key("group_exp_2"), + ) as mock_decision: + self.assertEqual( + decision_service.Decision(None, None, enums.DecisionSources.ROLLOUT), + self.decision_service.get_variation_for_feature( + self.project_config, feature, "test_user" + ), + ) + + mock_decision.assert_called_once_with( + self.project_config, self.project_config.get_group("19228"), "test_user" + ) - group = self.project_config.get_group('19228') - with mock.patch('optimizely.bucketer.Bucketer.find_bucket', return_value=None), \ - self.mock_decision_logger as mock_decision_service_logging: - self.assertIsNone(self.decision_service.get_experiment_in_group(self.project_config, group, 'test_user')) + def test_get_experiment_in_group(self): + """ Test that get_experiment_in_group returns the bucketed experiment for the user. """ + + group = self.project_config.get_group("19228") + experiment = self.project_config.get_experiment_from_id("32222") + with mock.patch( + "optimizely.bucketer.Bucketer.find_bucket", return_value="32222" + ), self.mock_decision_logger as mock_decision_service_logging: + self.assertEqual( + experiment, + self.decision_service.get_experiment_in_group( + self.project_config, group, "test_user" + ), + ) + + mock_decision_service_logging.info.assert_called_once_with( + 'User with bucketing ID "test_user" is in experiment group_exp_1 of group 19228.' + ) - mock_decision_service_logging.info.assert_called_once_with( - 'User with bucketing ID "test_user" is not in any experiments of group 19228.' - ) + def test_get_experiment_in_group__returns_none_if_user_not_in_group(self): + """ Test that get_experiment_in_group returns None if the user is not bucketed into the group. """ + + group = self.project_config.get_group("19228") + with mock.patch( + "optimizely.bucketer.Bucketer.find_bucket", return_value=None + ), self.mock_decision_logger as mock_decision_service_logging: + self.assertIsNone( + self.decision_service.get_experiment_in_group( + self.project_config, group, "test_user" + ) + ) + + mock_decision_service_logging.info.assert_called_once_with( + 'User with bucketing ID "test_user" is not in any experiments of group 19228.' + ) diff --git a/tests/test_event_builder.py b/tests/test_event_builder.py index 32c8e44e..6147c9db 100644 --- a/tests/test_event_builder.py +++ b/tests/test_event_builder.py @@ -21,745 +21,844 @@ class EventTest(unittest.TestCase): - - def test_init(self): - url = 'event.optimizely.com' - params = { - 'a': '111001', - 'n': 'test_event', - 'g': '111028', - 'u': 'oeutest_user' - } - http_verb = 'POST' - headers = {'Content-Type': 'application/json'} - event_obj = event_builder.Event(url, params, http_verb=http_verb, headers=headers) - self.assertEqual(url, event_obj.url) - self.assertEqual(params, event_obj.params) - self.assertEqual(http_verb, event_obj.http_verb) - self.assertEqual(headers, event_obj.headers) + def test_init(self): + url = 'event.optimizely.com' + params = {'a': '111001', 'n': 'test_event', 'g': '111028', 'u': 'oeutest_user'} + http_verb = 'POST' + headers = {'Content-Type': 'application/json'} + event_obj = event_builder.Event(url, params, http_verb=http_verb, headers=headers) + self.assertEqual(url, event_obj.url) + self.assertEqual(params, event_obj.params) + self.assertEqual(http_verb, event_obj.http_verb) + self.assertEqual(headers, event_obj.headers) class EventBuilderTest(base.BaseTest): + def setUp(self, *args, **kwargs): + base.BaseTest.setUp(self, 'config_dict_with_multiple_experiments') + self.event_builder = self.optimizely.event_builder + + def _validate_event_object(self, event_obj, expected_url, expected_params, expected_verb, expected_headers): + """ Helper method to validate properties of the event object. """ + + self.assertEqual(expected_url, event_obj.url) + + expected_params['visitors'][0]['attributes'] = sorted( + expected_params['visitors'][0]['attributes'], key=itemgetter('key') + ) + event_obj.params['visitors'][0]['attributes'] = sorted( + event_obj.params['visitors'][0]['attributes'], key=itemgetter('key') + ) + self.assertEqual(expected_params, event_obj.params) + + self.assertEqual(expected_verb, event_obj.http_verb) + self.assertEqual(expected_headers, event_obj.headers) + + def test_create_impression_event(self): + """ Test that create_impression_event creates Event object with right params. """ + + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [], + 'snapshots': [ + { + 'decisions': [ + {'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'} + ], + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'optimizely.bucketer.Bucketer._generate_bucket_value', return_value=5042 + ), mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'): + event_obj = self.event_builder.create_impression_event( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + '111129', + 'test_user', + None, + ) + self._validate_event_object( + event_obj, + event_builder.EventBuilder.EVENTS_URL, + expected_params, + event_builder.EventBuilder.HTTP_VERB, + event_builder.EventBuilder.HTTP_HEADERS, + ) - def setUp(self, *args, **kwargs): - base.BaseTest.setUp(self, 'config_dict_with_multiple_experiments') - self.event_builder = self.optimizely.event_builder - - def _validate_event_object(self, event_obj, expected_url, expected_params, expected_verb, expected_headers): - """ Helper method to validate properties of the event object. """ - - self.assertEqual(expected_url, event_obj.url) - - expected_params['visitors'][0]['attributes'] = \ - sorted(expected_params['visitors'][0]['attributes'], key=itemgetter('key')) - event_obj.params['visitors'][0]['attributes'] = \ - sorted(event_obj.params['visitors'][0]['attributes'], key=itemgetter('key')) - self.assertEqual(expected_params, event_obj.params) - - self.assertEqual(expected_verb, event_obj.http_verb) - self.assertEqual(expected_headers, event_obj.headers) - - def test_create_impression_event(self): - """ Test that create_impression_event creates Event object with right params. """ - - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [], - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111129', - 'experiment_id': '111127', - 'campaign_id': '111182' - }], - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('optimizely.bucketer.Bucketer._generate_bucket_value', return_value=5042), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'): - event_obj = self.event_builder.create_impression_event( - self.project_config, self.project_config.get_experiment_from_key('test_experiment'), '111129', 'test_user', None - ) - self._validate_event_object(event_obj, - event_builder.EventBuilder.EVENTS_URL, - expected_params, - event_builder.EventBuilder.HTTP_VERB, - event_builder.EventBuilder.HTTP_HEADERS) - - def test_create_impression_event__with_attributes(self): - """ Test that create_impression_event creates Event object + def test_create_impression_event__with_attributes(self): + """ Test that create_impression_event creates Event object with right params when attributes are provided. """ - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 'test_value', - 'entity_id': '111094', - 'key': 'test_attribute' - }], - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111129', - 'experiment_id': '111127', - 'campaign_id': '111182' - }], - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'): - event_obj = self.event_builder.create_impression_event( - self.project_config, self.project_config.get_experiment_from_key('test_experiment'), - '111129', 'test_user', {'test_attribute': 'test_value'} - ) - self._validate_event_object(event_obj, - event_builder.EventBuilder.EVENTS_URL, - expected_params, - event_builder.EventBuilder.HTTP_VERB, - event_builder.EventBuilder.HTTP_HEADERS) - - def test_create_impression_event_when_attribute_is_not_in_datafile(self): - """ Test that create_impression_event creates Event object + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + {'type': 'custom', 'value': 'test_value', 'entity_id': '111094', 'key': 'test_attribute'} + ], + 'snapshots': [ + { + 'decisions': [ + {'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'} + ], + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ): + event_obj = self.event_builder.create_impression_event( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + '111129', + 'test_user', + {'test_attribute': 'test_value'}, + ) + self._validate_event_object( + event_obj, + event_builder.EventBuilder.EVENTS_URL, + expected_params, + event_builder.EventBuilder.HTTP_VERB, + event_builder.EventBuilder.HTTP_HEADERS, + ) + + def test_create_impression_event_when_attribute_is_not_in_datafile(self): + """ Test that create_impression_event creates Event object with right params when attribute is not in the datafile. """ - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [], - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111129', - 'experiment_id': '111127', - 'campaign_id': '111182' - }], - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'): - event_obj = self.event_builder.create_impression_event( - self.project_config, self.project_config.get_experiment_from_key('test_experiment'), - '111129', 'test_user', {'do_you_know_me': 'test_value'} + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [], + 'snapshots': [ + { + 'decisions': [ + {'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'} + ], + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ): + event_obj = self.event_builder.create_impression_event( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + '111129', + 'test_user', + {'do_you_know_me': 'test_value'}, + ) + self._validate_event_object( + event_obj, + event_builder.EventBuilder.EVENTS_URL, + expected_params, + event_builder.EventBuilder.HTTP_VERB, + event_builder.EventBuilder.HTTP_HEADERS, ) - self._validate_event_object(event_obj, - event_builder.EventBuilder.EVENTS_URL, - expected_params, - event_builder.EventBuilder.HTTP_VERB, - event_builder.EventBuilder.HTTP_HEADERS) - - def test_create_impression_event_calls_is_attribute_valid(self): - """ Test that create_impression_event calls is_attribute_valid and + + def test_create_impression_event_calls_is_attribute_valid(self): + """ Test that create_impression_event calls is_attribute_valid and creates Event object with only those attributes for which is_attribute_valid is True.""" - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 5.5, - 'entity_id': '111198', - 'key': 'double_key' - }, { - 'type': 'custom', - 'value': True, - 'entity_id': '111196', - 'key': 'boolean_key' - }], - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111129', - 'experiment_id': '111127', - 'campaign_id': '111182' - }], - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - def side_effect(*args, **kwargs): - attribute_key = args[0] - if attribute_key == 'boolean_key' or attribute_key == 'double_key': - return True - - return False - - attributes = { - 'test_attribute': 'test_value', - 'boolean_key': True, - 'integer_key': 0, - 'double_key': 5.5 - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'),\ - mock.patch('optimizely.helpers.validator.is_attribute_valid', side_effect=side_effect): - - event_obj = self.event_builder.create_impression_event( - self.project_config, self.project_config.get_experiment_from_key('test_experiment'), - '111129', 'test_user', attributes - ) - - self._validate_event_object(event_obj, - event_builder.EventBuilder.EVENTS_URL, - expected_params, - event_builder.EventBuilder.HTTP_VERB, - event_builder.EventBuilder.HTTP_HEADERS) - - def test_create_impression_event__with_user_agent_when_bot_filtering_is_enabled(self): - """ Test that create_impression_event creates Event object + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + {'type': 'custom', 'value': 5.5, 'entity_id': '111198', 'key': 'double_key'}, + {'type': 'custom', 'value': True, 'entity_id': '111196', 'key': 'boolean_key'}, + ], + 'snapshots': [ + { + 'decisions': [ + {'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'} + ], + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + def side_effect(*args, **kwargs): + attribute_key = args[0] + if attribute_key == 'boolean_key' or attribute_key == 'double_key': + return True + + return False + + attributes = { + 'test_attribute': 'test_value', + 'boolean_key': True, + 'integer_key': 0, + 'double_key': 5.5, + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ), mock.patch('optimizely.helpers.validator.is_attribute_valid', side_effect=side_effect): + + event_obj = self.event_builder.create_impression_event( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + '111129', + 'test_user', + attributes, + ) + + self._validate_event_object( + event_obj, + event_builder.EventBuilder.EVENTS_URL, + expected_params, + event_builder.EventBuilder.HTTP_VERB, + event_builder.EventBuilder.HTTP_HEADERS, + ) + + def test_create_impression_event__with_user_agent_when_bot_filtering_is_enabled(self,): + """ Test that create_impression_event creates Event object with right params when user agent attribute is provided and bot filtering is enabled """ - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 'Edge', - 'entity_id': '$opt_user_agent', - 'key': '$opt_user_agent' - }, { - 'type': 'custom', - 'value': True, - 'entity_id': '$opt_bot_filtering', - 'key': '$opt_bot_filtering' - }], - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111129', - 'experiment_id': '111127', - 'campaign_id': '111182' - }], - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'),\ - mock.patch('optimizely.project_config.ProjectConfig.get_bot_filtering_value', return_value=True): - event_obj = self.event_builder.create_impression_event( - self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), - '111129', 'test_user', {'$opt_user_agent': 'Edge'} - ) - - self._validate_event_object(event_obj, - event_builder.EventBuilder.EVENTS_URL, - expected_params, - event_builder.EventBuilder.HTTP_VERB, - event_builder.EventBuilder.HTTP_HEADERS) - - def test_create_impression_event__with_empty_attributes_when_bot_filtering_is_enabled(self): - """ Test that create_impression_event creates Event object + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + {'type': 'custom', 'value': 'Edge', 'entity_id': '$opt_user_agent', 'key': '$opt_user_agent'}, + { + 'type': 'custom', + 'value': True, + 'entity_id': '$opt_bot_filtering', + 'key': '$opt_bot_filtering', + }, + ], + 'snapshots': [ + { + 'decisions': [ + {'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'} + ], + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ), mock.patch( + 'optimizely.project_config.ProjectConfig.get_bot_filtering_value', return_value=True, + ): + event_obj = self.event_builder.create_impression_event( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + '111129', + 'test_user', + {'$opt_user_agent': 'Edge'}, + ) + + self._validate_event_object( + event_obj, + event_builder.EventBuilder.EVENTS_URL, + expected_params, + event_builder.EventBuilder.HTTP_VERB, + event_builder.EventBuilder.HTTP_HEADERS, + ) + + def test_create_impression_event__with_empty_attributes_when_bot_filtering_is_enabled(self,): + """ Test that create_impression_event creates Event object with right params when empty attributes are provided and bot filtering is enabled """ - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': True, - 'entity_id': '$opt_bot_filtering', - 'key': '$opt_bot_filtering' - }], - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111129', - 'experiment_id': '111127', - 'campaign_id': '111182' - }], - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'),\ - mock.patch('optimizely.project_config.ProjectConfig.get_bot_filtering_value', return_value=True): - event_obj = self.event_builder.create_impression_event( - self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), - '111129', 'test_user', None - ) - - self._validate_event_object(event_obj, - event_builder.EventBuilder.EVENTS_URL, - expected_params, - event_builder.EventBuilder.HTTP_VERB, - event_builder.EventBuilder.HTTP_HEADERS) - - def test_create_impression_event__with_user_agent_when_bot_filtering_is_disabled(self): - """ Test that create_impression_event creates Event object + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + { + 'type': 'custom', + 'value': True, + 'entity_id': '$opt_bot_filtering', + 'key': '$opt_bot_filtering', + } + ], + 'snapshots': [ + { + 'decisions': [ + {'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'} + ], + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ), mock.patch( + 'optimizely.project_config.ProjectConfig.get_bot_filtering_value', return_value=True, + ): + event_obj = self.event_builder.create_impression_event( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + '111129', + 'test_user', + None, + ) + + self._validate_event_object( + event_obj, + event_builder.EventBuilder.EVENTS_URL, + expected_params, + event_builder.EventBuilder.HTTP_VERB, + event_builder.EventBuilder.HTTP_HEADERS, + ) + + def test_create_impression_event__with_user_agent_when_bot_filtering_is_disabled(self,): + """ Test that create_impression_event creates Event object with right params when user agent attribute is provided and bot filtering is disabled """ - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 'Chrome', - 'entity_id': '$opt_user_agent', - 'key': '$opt_user_agent' - }, { - 'type': 'custom', - 'value': False, - 'entity_id': '$opt_bot_filtering', - 'key': '$opt_bot_filtering' - }], - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111129', - 'experiment_id': '111127', - 'campaign_id': '111182' - }], - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'),\ - mock.patch('optimizely.project_config.ProjectConfig.get_bot_filtering_value', return_value=False): - event_obj = self.event_builder.create_impression_event( - self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), - '111129', 'test_user', {'$opt_user_agent': 'Chrome'} - ) - - self._validate_event_object(event_obj, - event_builder.EventBuilder.EVENTS_URL, - expected_params, - event_builder.EventBuilder.HTTP_VERB, - event_builder.EventBuilder.HTTP_HEADERS) - - def test_create_conversion_event(self): - """ Test that create_conversion_event creates Event object + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + { + 'type': 'custom', + 'value': 'Chrome', + 'entity_id': '$opt_user_agent', + 'key': '$opt_user_agent', + }, + { + 'type': 'custom', + 'value': False, + 'entity_id': '$opt_bot_filtering', + 'key': '$opt_bot_filtering', + }, + ], + 'snapshots': [ + { + 'decisions': [ + {'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'} + ], + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ), mock.patch( + 'optimizely.project_config.ProjectConfig.get_bot_filtering_value', return_value=False, + ): + event_obj = self.event_builder.create_impression_event( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + '111129', + 'test_user', + {'$opt_user_agent': 'Chrome'}, + ) + + self._validate_event_object( + event_obj, + event_builder.EventBuilder.EVENTS_URL, + expected_params, + event_builder.EventBuilder.HTTP_VERB, + event_builder.EventBuilder.HTTP_HEADERS, + ) + + def test_create_conversion_event(self): + """ Test that create_conversion_event creates Event object with right params when no attributes are provided. """ - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [], - 'snapshots': [{ - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111095', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'test_event' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'): - event_obj = self.event_builder.create_conversion_event( - self.project_config, 'test_event', 'test_user', None, None - ) - self._validate_event_object(event_obj, - event_builder.EventBuilder.EVENTS_URL, - expected_params, - event_builder.EventBuilder.HTTP_VERB, - event_builder.EventBuilder.HTTP_HEADERS) - - def test_create_conversion_event__with_attributes(self): - """ Test that create_conversion_event creates Event object + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [], + 'snapshots': [ + { + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111095', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'test_event', + } + ] + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ): + event_obj = self.event_builder.create_conversion_event( + self.project_config, 'test_event', 'test_user', None, None + ) + self._validate_event_object( + event_obj, + event_builder.EventBuilder.EVENTS_URL, + expected_params, + event_builder.EventBuilder.HTTP_VERB, + event_builder.EventBuilder.HTTP_HEADERS, + ) + + def test_create_conversion_event__with_attributes(self): + """ Test that create_conversion_event creates Event object with right params when attributes are provided. """ - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 'test_value', - 'entity_id': '111094', - 'key': 'test_attribute' - }], - 'snapshots': [{ - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111095', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'test_event' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'): - event_obj = self.event_builder.create_conversion_event( - self.project_config, 'test_event', 'test_user', {'test_attribute': 'test_value'}, None - ) - self._validate_event_object(event_obj, - event_builder.EventBuilder.EVENTS_URL, - expected_params, - event_builder.EventBuilder.HTTP_VERB, - event_builder.EventBuilder.HTTP_HEADERS) - - def test_create_conversion_event__with_user_agent_when_bot_filtering_is_enabled(self): - """ Test that create_conversion_event creates Event object + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + {'type': 'custom', 'value': 'test_value', 'entity_id': '111094', 'key': 'test_attribute'} + ], + 'snapshots': [ + { + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111095', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'test_event', + } + ] + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ): + event_obj = self.event_builder.create_conversion_event( + self.project_config, 'test_event', 'test_user', {'test_attribute': 'test_value'}, None, + ) + self._validate_event_object( + event_obj, + event_builder.EventBuilder.EVENTS_URL, + expected_params, + event_builder.EventBuilder.HTTP_VERB, + event_builder.EventBuilder.HTTP_HEADERS, + ) + + def test_create_conversion_event__with_user_agent_when_bot_filtering_is_enabled(self,): + """ Test that create_conversion_event creates Event object with right params when user agent attribute is provided and bot filtering is enabled """ - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 'Edge', - 'entity_id': '$opt_user_agent', - 'key': '$opt_user_agent' - }, { - 'type': 'custom', - 'value': True, - 'entity_id': '$opt_bot_filtering', - 'key': '$opt_bot_filtering' - }], - 'snapshots': [{ - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111095', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'test_event' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ - mock.patch('optimizely.project_config.ProjectConfig.get_bot_filtering_value', return_value=True): - event_obj = self.event_builder.create_conversion_event( - self.project_config, 'test_event', 'test_user', {'$opt_user_agent': 'Edge'}, None - ) - - self._validate_event_object(event_obj, - event_builder.EventBuilder.EVENTS_URL, - expected_params, - event_builder.EventBuilder.HTTP_VERB, - event_builder.EventBuilder.HTTP_HEADERS) - - def test_create_conversion_event__with_user_agent_when_bot_filtering_is_disabled(self): - """ Test that create_conversion_event creates Event object + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + {'type': 'custom', 'value': 'Edge', 'entity_id': '$opt_user_agent', 'key': '$opt_user_agent'}, + { + 'type': 'custom', + 'value': True, + 'entity_id': '$opt_bot_filtering', + 'key': '$opt_bot_filtering', + }, + ], + 'snapshots': [ + { + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111095', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'test_event', + } + ] + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ), mock.patch( + 'optimizely.project_config.ProjectConfig.get_bot_filtering_value', return_value=True, + ): + event_obj = self.event_builder.create_conversion_event( + self.project_config, 'test_event', 'test_user', {'$opt_user_agent': 'Edge'}, None, + ) + + self._validate_event_object( + event_obj, + event_builder.EventBuilder.EVENTS_URL, + expected_params, + event_builder.EventBuilder.HTTP_VERB, + event_builder.EventBuilder.HTTP_HEADERS, + ) + + def test_create_conversion_event__with_user_agent_when_bot_filtering_is_disabled(self,): + """ Test that create_conversion_event creates Event object with right params when user agent attribute is provided and bot filtering is disabled """ - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 'Chrome', - 'entity_id': '$opt_user_agent', - 'key': '$opt_user_agent' - }, { - 'type': 'custom', - 'value': False, - 'entity_id': '$opt_bot_filtering', - 'key': '$opt_bot_filtering' - }], - 'snapshots': [{ - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111095', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'test_event' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ - mock.patch('optimizely.project_config.ProjectConfig.get_bot_filtering_value', return_value=False): - event_obj = self.event_builder.create_conversion_event( - self.project_config, 'test_event', 'test_user', {'$opt_user_agent': 'Chrome'}, None - ) - - self._validate_event_object(event_obj, - event_builder.EventBuilder.EVENTS_URL, - expected_params, - event_builder.EventBuilder.HTTP_VERB, - event_builder.EventBuilder.HTTP_HEADERS) - - def test_create_conversion_event__with_event_tags(self): - """ Test that create_conversion_event creates Event object + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + { + 'type': 'custom', + 'value': 'Chrome', + 'entity_id': '$opt_user_agent', + 'key': '$opt_user_agent', + }, + { + 'type': 'custom', + 'value': False, + 'entity_id': '$opt_bot_filtering', + 'key': '$opt_bot_filtering', + }, + ], + 'snapshots': [ + { + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111095', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'test_event', + } + ] + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ), mock.patch( + 'optimizely.project_config.ProjectConfig.get_bot_filtering_value', return_value=False, + ): + event_obj = self.event_builder.create_conversion_event( + self.project_config, 'test_event', 'test_user', {'$opt_user_agent': 'Chrome'}, None, + ) + + self._validate_event_object( + event_obj, + event_builder.EventBuilder.EVENTS_URL, + expected_params, + event_builder.EventBuilder.HTTP_VERB, + event_builder.EventBuilder.HTTP_HEADERS, + ) + + def test_create_conversion_event__with_event_tags(self): + """ Test that create_conversion_event creates Event object with right params when event tags are provided. """ - expected_params = { - 'client_version': version.__version__, - 'project_id': '111001', - 'visitors': [{ - 'attributes': [{ - 'entity_id': '111094', - 'type': 'custom', - 'value': 'test_value', - 'key': 'test_attribute' - }], - 'visitor_id': 'test_user', - 'snapshots': [{ - 'events': [{ - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'tags': { - 'non-revenue': 'abc', - 'revenue': 4200, - 'value': 1.234 - }, - 'timestamp': 42123, - 'revenue': 4200, - 'value': 1.234, - 'key': 'test_event', - 'entity_id': '111095' - }] - }] - }], - 'account_id': '12001', - 'client_name': 'python-sdk', - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'): - event_obj = self.event_builder.create_conversion_event( - self.project_config, - 'test_event', - 'test_user', - {'test_attribute': 'test_value'}, - {'revenue': 4200, 'value': 1.234, 'non-revenue': 'abc'} - ) - self._validate_event_object(event_obj, - event_builder.EventBuilder.EVENTS_URL, - expected_params, - event_builder.EventBuilder.HTTP_VERB, - event_builder.EventBuilder.HTTP_HEADERS) - - def test_create_conversion_event__with_invalid_event_tags(self): - """ Test that create_conversion_event creates Event object + expected_params = { + 'client_version': version.__version__, + 'project_id': '111001', + 'visitors': [ + { + 'attributes': [ + {'entity_id': '111094', 'type': 'custom', 'value': 'test_value', 'key': 'test_attribute'} + ], + 'visitor_id': 'test_user', + 'snapshots': [ + { + 'events': [ + { + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'tags': {'non-revenue': 'abc', 'revenue': 4200, 'value': 1.234}, + 'timestamp': 42123, + 'revenue': 4200, + 'value': 1.234, + 'key': 'test_event', + 'entity_id': '111095', + } + ] + } + ], + } + ], + 'account_id': '12001', + 'client_name': 'python-sdk', + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ): + event_obj = self.event_builder.create_conversion_event( + self.project_config, + 'test_event', + 'test_user', + {'test_attribute': 'test_value'}, + {'revenue': 4200, 'value': 1.234, 'non-revenue': 'abc'}, + ) + self._validate_event_object( + event_obj, + event_builder.EventBuilder.EVENTS_URL, + expected_params, + event_builder.EventBuilder.HTTP_VERB, + event_builder.EventBuilder.HTTP_HEADERS, + ) + + def test_create_conversion_event__with_invalid_event_tags(self): + """ Test that create_conversion_event creates Event object with right params when event tags are provided. """ - expected_params = { - 'client_version': version.__version__, - 'project_id': '111001', - 'visitors': [{ - 'attributes': [{ - 'entity_id': '111094', - 'type': 'custom', - 'value': 'test_value', - 'key': 'test_attribute' - }], - 'visitor_id': 'test_user', - 'snapshots': [{ - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111095', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'test_event', - 'tags': { - 'non-revenue': 'abc', - 'revenue': '4200', - 'value': True - } - }] - }] - }], - 'account_id': '12001', - 'client_name': 'python-sdk', - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'): - event_obj = self.event_builder.create_conversion_event( - self.project_config, - 'test_event', - 'test_user', - {'test_attribute': 'test_value'}, - {'revenue': '4200', 'value': True, 'non-revenue': 'abc'} - ) - self._validate_event_object(event_obj, - event_builder.EventBuilder.EVENTS_URL, - expected_params, - event_builder.EventBuilder.HTTP_VERB, - event_builder.EventBuilder.HTTP_HEADERS) - - def test_create_conversion_event__when_event_is_used_in_multiple_experiments(self): - """ Test that create_conversion_event creates Event object with + expected_params = { + 'client_version': version.__version__, + 'project_id': '111001', + 'visitors': [ + { + 'attributes': [ + {'entity_id': '111094', 'type': 'custom', 'value': 'test_value', 'key': 'test_attribute'} + ], + 'visitor_id': 'test_user', + 'snapshots': [ + { + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111095', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'test_event', + 'tags': {'non-revenue': 'abc', 'revenue': '4200', 'value': True}, + } + ] + } + ], + } + ], + 'account_id': '12001', + 'client_name': 'python-sdk', + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ): + event_obj = self.event_builder.create_conversion_event( + self.project_config, + 'test_event', + 'test_user', + {'test_attribute': 'test_value'}, + {'revenue': '4200', 'value': True, 'non-revenue': 'abc'}, + ) + self._validate_event_object( + event_obj, + event_builder.EventBuilder.EVENTS_URL, + expected_params, + event_builder.EventBuilder.HTTP_VERB, + event_builder.EventBuilder.HTTP_HEADERS, + ) + + def test_create_conversion_event__when_event_is_used_in_multiple_experiments(self): + """ Test that create_conversion_event creates Event object with right params when multiple experiments use the same event. """ - expected_params = { - 'client_version': version.__version__, - 'project_id': '111001', - 'visitors': [{ - 'attributes': [{ - 'entity_id': '111094', - 'type': 'custom', - 'value': 'test_value', - 'key': 'test_attribute' - }], - 'visitor_id': 'test_user', - 'snapshots': [{ - 'events': [{ - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'tags': { - 'non-revenue': 'abc', - 'revenue': 4200, - 'value': 1.234 - }, - 'timestamp': 42123, - 'revenue': 4200, - 'value': 1.234, - 'key': 'test_event', - 'entity_id': '111095' - }] - }] - }], - 'account_id': '12001', - 'client_name': 'python-sdk', - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'): - event_obj = self.event_builder.create_conversion_event( - self.project_config, - 'test_event', - 'test_user', - {'test_attribute': 'test_value'}, - {'revenue': 4200, 'value': 1.234, 'non-revenue': 'abc'} - ) - self._validate_event_object(event_obj, - event_builder.EventBuilder.EVENTS_URL, - expected_params, - event_builder.EventBuilder.HTTP_VERB, - event_builder.EventBuilder.HTTP_HEADERS) + expected_params = { + 'client_version': version.__version__, + 'project_id': '111001', + 'visitors': [ + { + 'attributes': [ + {'entity_id': '111094', 'type': 'custom', 'value': 'test_value', 'key': 'test_attribute'} + ], + 'visitor_id': 'test_user', + 'snapshots': [ + { + 'events': [ + { + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'tags': {'non-revenue': 'abc', 'revenue': 4200, 'value': 1.234}, + 'timestamp': 42123, + 'revenue': 4200, + 'value': 1.234, + 'key': 'test_event', + 'entity_id': '111095', + } + ] + } + ], + } + ], + 'account_id': '12001', + 'client_name': 'python-sdk', + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ): + event_obj = self.event_builder.create_conversion_event( + self.project_config, + 'test_event', + 'test_user', + {'test_attribute': 'test_value'}, + {'revenue': 4200, 'value': 1.234, 'non-revenue': 'abc'}, + ) + self._validate_event_object( + event_obj, + event_builder.EventBuilder.EVENTS_URL, + expected_params, + event_builder.EventBuilder.HTTP_VERB, + event_builder.EventBuilder.HTTP_HEADERS, + ) diff --git a/tests/test_event_dispatcher.py b/tests/test_event_dispatcher.py index a6ce0456..15e89180 100644 --- a/tests/test_event_dispatcher.py +++ b/tests/test_event_dispatcher.py @@ -21,61 +21,61 @@ class EventDispatcherTest(unittest.TestCase): - - def test_dispatch_event__get_request(self): - """ Test that dispatch event fires off requests call with provided URL and params. """ - - url = 'https://www.optimizely.com' - params = { - 'a': '111001', - 'n': 'test_event', - 'g': '111028', - 'u': 'oeutest_user' - } - event = event_builder.Event(url, params) - - with mock.patch('requests.get') as mock_request_get: - event_dispatcher.EventDispatcher.dispatch_event(event) - - mock_request_get.assert_called_once_with(url, params=params, timeout=event_dispatcher.REQUEST_TIMEOUT) - - def test_dispatch_event__post_request(self): - """ Test that dispatch event fires off requests call with provided URL, params, HTTP verb and headers. """ - - url = 'https://www.optimizely.com' - params = { - 'accountId': '111001', - 'eventName': 'test_event', - 'eventEntityId': '111028', - 'visitorId': 'oeutest_user' - } - event = event_builder.Event(url, params, http_verb='POST', headers={'Content-Type': 'application/json'}) - - with mock.patch('requests.post') as mock_request_post: - event_dispatcher.EventDispatcher.dispatch_event(event) - - mock_request_post.assert_called_once_with(url, data=json.dumps(params), - headers={'Content-Type': 'application/json'}, - timeout=event_dispatcher.REQUEST_TIMEOUT) - - def test_dispatch_event__handle_request_exception(self): - """ Test that dispatch event handles exceptions and logs error. """ - - url = 'https://www.optimizely.com' - params = { - 'accountId': '111001', - 'eventName': 'test_event', - 'eventEntityId': '111028', - 'visitorId': 'oeutest_user' - } - event = event_builder.Event(url, params, http_verb='POST', headers={'Content-Type': 'application/json'}) - - with mock.patch('requests.post', - side_effect=request_exception.RequestException('Failed Request')) as mock_request_post,\ - mock.patch('logging.error') as mock_log_error: - event_dispatcher.EventDispatcher.dispatch_event(event) - - mock_request_post.assert_called_once_with(url, data=json.dumps(params), - headers={'Content-Type': 'application/json'}, - timeout=event_dispatcher.REQUEST_TIMEOUT) - mock_log_error.assert_called_once_with('Dispatch event failed. Error: Failed Request') + def test_dispatch_event__get_request(self): + """ Test that dispatch event fires off requests call with provided URL and params. """ + + url = 'https://www.optimizely.com' + params = {'a': '111001', 'n': 'test_event', 'g': '111028', 'u': 'oeutest_user'} + event = event_builder.Event(url, params) + + with mock.patch('requests.get') as mock_request_get: + event_dispatcher.EventDispatcher.dispatch_event(event) + + mock_request_get.assert_called_once_with(url, params=params, timeout=event_dispatcher.REQUEST_TIMEOUT) + + def test_dispatch_event__post_request(self): + """ Test that dispatch event fires off requests call with provided URL, params, HTTP verb and headers. """ + + url = 'https://www.optimizely.com' + params = { + 'accountId': '111001', + 'eventName': 'test_event', + 'eventEntityId': '111028', + 'visitorId': 'oeutest_user', + } + event = event_builder.Event(url, params, http_verb='POST', headers={'Content-Type': 'application/json'}) + + with mock.patch('requests.post') as mock_request_post: + event_dispatcher.EventDispatcher.dispatch_event(event) + + mock_request_post.assert_called_once_with( + url, + data=json.dumps(params), + headers={'Content-Type': 'application/json'}, + timeout=event_dispatcher.REQUEST_TIMEOUT, + ) + + def test_dispatch_event__handle_request_exception(self): + """ Test that dispatch event handles exceptions and logs error. """ + + url = 'https://www.optimizely.com' + params = { + 'accountId': '111001', + 'eventName': 'test_event', + 'eventEntityId': '111028', + 'visitorId': 'oeutest_user', + } + event = event_builder.Event(url, params, http_verb='POST', headers={'Content-Type': 'application/json'}) + + with mock.patch( + 'requests.post', side_effect=request_exception.RequestException('Failed Request'), + ) as mock_request_post, mock.patch('logging.error') as mock_log_error: + event_dispatcher.EventDispatcher.dispatch_event(event) + + mock_request_post.assert_called_once_with( + url, + data=json.dumps(params), + headers={'Content-Type': 'application/json'}, + timeout=event_dispatcher.REQUEST_TIMEOUT, + ) + mock_log_error.assert_called_once_with('Dispatch event failed. Error: Failed Request') diff --git a/tests/test_event_factory.py b/tests/test_event_factory.py index bc89fa21..73a8054b 100644 --- a/tests/test_event_factory.py +++ b/tests/test_event_factory.py @@ -26,783 +26,832 @@ class LogEventTest(unittest.TestCase): - - def test_init(self): - url = 'event.optimizely.com' - params = { - 'a': '111001', - 'n': 'test_event', - 'g': '111028', - 'u': 'oeutest_user' - } - http_verb = 'POST' - headers = {'Content-Type': 'application/json'} - event_obj = LogEvent(url, params, http_verb=http_verb, headers=headers) - self.assertEqual(url, event_obj.url) - self.assertEqual(params, event_obj.params) - self.assertEqual(http_verb, event_obj.http_verb) - self.assertEqual(headers, event_obj.headers) + def test_init(self): + url = 'event.optimizely.com' + params = {'a': '111001', 'n': 'test_event', 'g': '111028', 'u': 'oeutest_user'} + http_verb = 'POST' + headers = {'Content-Type': 'application/json'} + event_obj = LogEvent(url, params, http_verb=http_verb, headers=headers) + self.assertEqual(url, event_obj.url) + self.assertEqual(params, event_obj.params) + self.assertEqual(http_verb, event_obj.http_verb) + self.assertEqual(headers, event_obj.headers) class EventFactoryTest(base.BaseTest): + def setUp(self, *args, **kwargs): + base.BaseTest.setUp(self, 'config_dict_with_multiple_experiments') + self.logger = logger.NoOpLogger() + self.uuid = str(uuid.uuid4()) + self.timestamp = int(round(time.time() * 1000)) - def setUp(self, *args, **kwargs): - base.BaseTest.setUp(self, 'config_dict_with_multiple_experiments') - self.logger = logger.NoOpLogger() - self.uuid = str(uuid.uuid4()) - self.timestamp = int(round(time.time() * 1000)) - - def _validate_event_object(self, event_obj, expected_url, expected_params, expected_verb, expected_headers): - """ Helper method to validate properties of the event object. """ - - self.assertEqual(expected_url, event_obj.url) - - expected_params['visitors'][0]['attributes'] = \ - sorted(expected_params['visitors'][0]['attributes'], key=itemgetter('key')) - event_obj.params['visitors'][0]['attributes'] = \ - sorted(event_obj.params['visitors'][0]['attributes'], key=itemgetter('key')) - self.assertEqual(expected_params, event_obj.params) - - self.assertEqual(expected_verb, event_obj.http_verb) - self.assertEqual(expected_headers, event_obj.headers) - - def test_create_impression_event(self): - """ Test that create_impression_event creates LogEvent object with right params. """ - - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [], - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111129', - 'experiment_id': '111127', - 'campaign_id': '111182' - }], - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'): - event_obj = UserEventFactory.create_impression_event( - self.project_config, self.project_config.get_experiment_from_key('test_experiment'), - '111129', 'test_user', None - ) - - log_event = EventFactory.create_log_event(event_obj, self.logger) - - self._validate_event_object(log_event, - EventFactory.EVENT_ENDPOINT, - expected_params, - EventFactory.HTTP_VERB, - EventFactory.HTTP_HEADERS) - - def test_create_impression_event__with_attributes(self): - """ Test that create_impression_event creates Event object - with right params when attributes are provided. """ + def _validate_event_object(self, event_obj, expected_url, expected_params, expected_verb, expected_headers): + """ Helper method to validate properties of the event object. """ - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 'test_value', - 'entity_id': '111094', - 'key': 'test_attribute' - }], - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111129', - 'experiment_id': '111127', - 'campaign_id': '111182' - }], - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'): - event_obj = UserEventFactory.create_impression_event( - self.project_config, self.project_config.get_experiment_from_key('test_experiment'), - '111129', 'test_user', {'test_attribute': 'test_value'} - ) - - log_event = EventFactory.create_log_event(event_obj, self.logger) - - self._validate_event_object(log_event, - EventFactory.EVENT_ENDPOINT, - expected_params, - EventFactory.HTTP_VERB, - EventFactory.HTTP_HEADERS) - - def test_create_impression_event_when_attribute_is_not_in_datafile(self): - """ Test that create_impression_event creates Event object - with right params when attribute is not in the datafile. """ + self.assertEqual(expected_url, event_obj.url) - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [], - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111129', - 'experiment_id': '111127', - 'campaign_id': '111182' - }], - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'): - event_obj = UserEventFactory.create_impression_event( - self.project_config, self.project_config.get_experiment_from_key('test_experiment'), - '111129', 'test_user', {'do_you_know_me': 'test_value'} + expected_params['visitors'][0]['attributes'] = sorted( + expected_params['visitors'][0]['attributes'], key=itemgetter('key') + ) + event_obj.params['visitors'][0]['attributes'] = sorted( + event_obj.params['visitors'][0]['attributes'], key=itemgetter('key') + ) + self.assertEqual(expected_params, event_obj.params) + + self.assertEqual(expected_verb, event_obj.http_verb) + self.assertEqual(expected_headers, event_obj.headers) + + def test_create_impression_event(self): + """ Test that create_impression_event creates LogEvent object with right params. """ + + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [], + 'snapshots': [ + { + 'decisions': [ + {'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'} + ], + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ): + event_obj = UserEventFactory.create_impression_event( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + '111129', + 'test_user', + None, + ) + + log_event = EventFactory.create_log_event(event_obj, self.logger) + + self._validate_event_object( + log_event, EventFactory.EVENT_ENDPOINT, expected_params, EventFactory.HTTP_VERB, EventFactory.HTTP_HEADERS, ) - log_event = EventFactory.create_log_event(event_obj, self.logger) + def test_create_impression_event__with_attributes(self): + """ Test that create_impression_event creates Event object + with right params when attributes are provided. """ - self._validate_event_object(log_event, - EventFactory.EVENT_ENDPOINT, - expected_params, - EventFactory.HTTP_VERB, - EventFactory.HTTP_HEADERS) + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + {'type': 'custom', 'value': 'test_value', 'entity_id': '111094', 'key': 'test_attribute'} + ], + 'snapshots': [ + { + 'decisions': [ + {'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'} + ], + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ): + event_obj = UserEventFactory.create_impression_event( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + '111129', + 'test_user', + {'test_attribute': 'test_value'}, + ) + + log_event = EventFactory.create_log_event(event_obj, self.logger) + + self._validate_event_object( + log_event, EventFactory.EVENT_ENDPOINT, expected_params, EventFactory.HTTP_VERB, EventFactory.HTTP_HEADERS, + ) - def test_create_impression_event_calls_is_attribute_valid(self): - """ Test that create_impression_event calls is_attribute_valid and - creates Event object with only those attributes for which is_attribute_valid is True.""" + def test_create_impression_event_when_attribute_is_not_in_datafile(self): + """ Test that create_impression_event creates Event object + with right params when attribute is not in the datafile. """ - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 5.5, - 'entity_id': '111198', - 'key': 'double_key' - }, { - 'type': 'custom', - 'value': True, - 'entity_id': '111196', - 'key': 'boolean_key' - }], - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111129', - 'experiment_id': '111127', - 'campaign_id': '111182' - }], - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - def side_effect(*args, **kwargs): - attribute_key = args[0] - if attribute_key == 'boolean_key' or attribute_key == 'double_key': - return True - - return False - - attributes = { - 'test_attribute': 'test_value', - 'boolean_key': True, - 'integer_key': 0, - 'double_key': 5.5 - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'),\ - mock.patch('optimizely.helpers.validator.is_attribute_valid', side_effect=side_effect): - - event_obj = UserEventFactory.create_impression_event( - self.project_config, self.project_config.get_experiment_from_key('test_experiment'), - '111129', 'test_user', attributes + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [], + 'snapshots': [ + { + 'decisions': [ + {'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'} + ], + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ): + event_obj = UserEventFactory.create_impression_event( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + '111129', + 'test_user', + {'do_you_know_me': 'test_value'}, + ) + + log_event = EventFactory.create_log_event(event_obj, self.logger) + + self._validate_event_object( + log_event, EventFactory.EVENT_ENDPOINT, expected_params, EventFactory.HTTP_VERB, EventFactory.HTTP_HEADERS, ) - log_event = EventFactory.create_log_event(event_obj, self.logger) + def test_create_impression_event_calls_is_attribute_valid(self): + """ Test that create_impression_event calls is_attribute_valid and + creates Event object with only those attributes for which is_attribute_valid is True.""" - self._validate_event_object(log_event, - EventFactory.EVENT_ENDPOINT, - expected_params, - EventFactory.HTTP_VERB, - EventFactory.HTTP_HEADERS) + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + {'type': 'custom', 'value': 5.5, 'entity_id': '111198', 'key': 'double_key'}, + {'type': 'custom', 'value': True, 'entity_id': '111196', 'key': 'boolean_key'}, + ], + 'snapshots': [ + { + 'decisions': [ + {'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'} + ], + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + def side_effect(*args, **kwargs): + attribute_key = args[0] + if attribute_key == 'boolean_key' or attribute_key == 'double_key': + return True + + return False + + attributes = { + 'test_attribute': 'test_value', + 'boolean_key': True, + 'integer_key': 0, + 'double_key': 5.5, + } - def test_create_impression_event__with_user_agent_when_bot_filtering_is_enabled(self): - """ Test that create_impression_event creates Event object + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ), mock.patch( + 'optimizely.helpers.validator.is_attribute_valid', side_effect=side_effect, + ): + + event_obj = UserEventFactory.create_impression_event( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + '111129', + 'test_user', + attributes, + ) + + log_event = EventFactory.create_log_event(event_obj, self.logger) + + self._validate_event_object( + log_event, + EventFactory.EVENT_ENDPOINT, + expected_params, + EventFactory.HTTP_VERB, + EventFactory.HTTP_HEADERS, + ) + + def test_create_impression_event__with_user_agent_when_bot_filtering_is_enabled(self,): + """ Test that create_impression_event creates Event object with right params when user agent attribute is provided and bot filtering is enabled """ - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 'Edge', - 'entity_id': '$opt_user_agent', - 'key': '$opt_user_agent' - }, { - 'type': 'custom', - 'value': True, - 'entity_id': '$opt_bot_filtering', - 'key': '$opt_bot_filtering' - }], - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111129', - 'experiment_id': '111127', - 'campaign_id': '111182' - }], - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'),\ - mock.patch('optimizely.project_config.ProjectConfig.get_bot_filtering_value', return_value=True): - event_obj = UserEventFactory.create_impression_event( - self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), - '111129', 'test_user', {'$opt_user_agent': 'Edge'} - ) - - log_event = EventFactory.create_log_event(event_obj, self.logger) - - self._validate_event_object(log_event, - EventFactory.EVENT_ENDPOINT, - expected_params, - EventFactory.HTTP_VERB, - EventFactory.HTTP_HEADERS) - - def test_create_impression_event__with_empty_attributes_when_bot_filtering_is_enabled(self): - """ Test that create_impression_event creates Event object + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + {'type': 'custom', 'value': 'Edge', 'entity_id': '$opt_user_agent', 'key': '$opt_user_agent'}, + { + 'type': 'custom', + 'value': True, + 'entity_id': '$opt_bot_filtering', + 'key': '$opt_bot_filtering', + }, + ], + 'snapshots': [ + { + 'decisions': [ + {'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'} + ], + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ), mock.patch( + 'optimizely.project_config.ProjectConfig.get_bot_filtering_value', return_value=True, + ): + event_obj = UserEventFactory.create_impression_event( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + '111129', + 'test_user', + {'$opt_user_agent': 'Edge'}, + ) + + log_event = EventFactory.create_log_event(event_obj, self.logger) + + self._validate_event_object( + log_event, EventFactory.EVENT_ENDPOINT, expected_params, EventFactory.HTTP_VERB, EventFactory.HTTP_HEADERS, + ) + + def test_create_impression_event__with_empty_attributes_when_bot_filtering_is_enabled(self,): + """ Test that create_impression_event creates Event object with right params when empty attributes are provided and bot filtering is enabled """ - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': True, - 'entity_id': '$opt_bot_filtering', - 'key': '$opt_bot_filtering' - }], - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111129', - 'experiment_id': '111127', - 'campaign_id': '111182' - }], - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'),\ - mock.patch('optimizely.project_config.ProjectConfig.get_bot_filtering_value', return_value=True): - event_obj = UserEventFactory.create_impression_event( - self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), - '111129', 'test_user', None - ) - - log_event = EventFactory.create_log_event(event_obj, self.logger) - - self._validate_event_object(log_event, - EventFactory.EVENT_ENDPOINT, - expected_params, - EventFactory.HTTP_VERB, - EventFactory.HTTP_HEADERS) - - def test_create_impression_event__with_user_agent_when_bot_filtering_is_disabled(self): - """ Test that create_impression_event creates Event object + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + { + 'type': 'custom', + 'value': True, + 'entity_id': '$opt_bot_filtering', + 'key': '$opt_bot_filtering', + } + ], + 'snapshots': [ + { + 'decisions': [ + {'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'} + ], + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ), mock.patch( + 'optimizely.project_config.ProjectConfig.get_bot_filtering_value', return_value=True, + ): + event_obj = UserEventFactory.create_impression_event( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + '111129', + 'test_user', + None, + ) + + log_event = EventFactory.create_log_event(event_obj, self.logger) + + self._validate_event_object( + log_event, EventFactory.EVENT_ENDPOINT, expected_params, EventFactory.HTTP_VERB, EventFactory.HTTP_HEADERS, + ) + + def test_create_impression_event__with_user_agent_when_bot_filtering_is_disabled(self,): + """ Test that create_impression_event creates Event object with right params when user agent attribute is provided and bot filtering is disabled """ - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 'Chrome', - 'entity_id': '$opt_user_agent', - 'key': '$opt_user_agent' - }, { - 'type': 'custom', - 'value': False, - 'entity_id': '$opt_bot_filtering', - 'key': '$opt_bot_filtering' - }], - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111129', - 'experiment_id': '111127', - 'campaign_id': '111182' - }], - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'),\ - mock.patch('optimizely.project_config.ProjectConfig.get_bot_filtering_value', return_value=False): - event_obj = UserEventFactory.create_impression_event( - self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), - '111129', 'test_user', {'$opt_user_agent': 'Chrome'} - ) - - log_event = EventFactory.create_log_event(event_obj, self.logger) - - self._validate_event_object(log_event, - EventFactory.EVENT_ENDPOINT, - expected_params, - EventFactory.HTTP_VERB, - EventFactory.HTTP_HEADERS) - - def test_create_conversion_event(self): - """ Test that create_conversion_event creates Event object + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + { + 'type': 'custom', + 'value': 'Chrome', + 'entity_id': '$opt_user_agent', + 'key': '$opt_user_agent', + }, + { + 'type': 'custom', + 'value': False, + 'entity_id': '$opt_bot_filtering', + 'key': '$opt_bot_filtering', + }, + ], + 'snapshots': [ + { + 'decisions': [ + {'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'} + ], + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ), mock.patch( + 'optimizely.project_config.ProjectConfig.get_bot_filtering_value', return_value=False, + ): + event_obj = UserEventFactory.create_impression_event( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + '111129', + 'test_user', + {'$opt_user_agent': 'Chrome'}, + ) + + log_event = EventFactory.create_log_event(event_obj, self.logger) + + self._validate_event_object( + log_event, EventFactory.EVENT_ENDPOINT, expected_params, EventFactory.HTTP_VERB, EventFactory.HTTP_HEADERS, + ) + + def test_create_conversion_event(self): + """ Test that create_conversion_event creates Event object with right params when no attributes are provided. """ - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [], - 'snapshots': [{ - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111095', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'test_event' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'): - event_obj = UserEventFactory.create_conversion_event( - self.project_config, 'test_event', 'test_user', None, None - ) - - log_event = EventFactory.create_log_event(event_obj, self.logger) - - self._validate_event_object(log_event, - EventFactory.EVENT_ENDPOINT, - expected_params, - EventFactory.HTTP_VERB, - EventFactory.HTTP_HEADERS) - - def test_create_conversion_event__with_attributes(self): - """ Test that create_conversion_event creates Event object + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [], + 'snapshots': [ + { + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111095', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'test_event', + } + ] + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ): + event_obj = UserEventFactory.create_conversion_event( + self.project_config, 'test_event', 'test_user', None, None + ) + + log_event = EventFactory.create_log_event(event_obj, self.logger) + + self._validate_event_object( + log_event, EventFactory.EVENT_ENDPOINT, expected_params, EventFactory.HTTP_VERB, EventFactory.HTTP_HEADERS, + ) + + def test_create_conversion_event__with_attributes(self): + """ Test that create_conversion_event creates Event object with right params when attributes are provided. """ - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 'test_value', - 'entity_id': '111094', - 'key': 'test_attribute' - }], - 'snapshots': [{ - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111095', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'test_event' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'): - event_obj = UserEventFactory.create_conversion_event( - self.project_config, 'test_event', 'test_user', {'test_attribute': 'test_value'}, None - ) - - log_event = EventFactory.create_log_event(event_obj, self.logger) - - self._validate_event_object(log_event, - EventFactory.EVENT_ENDPOINT, - expected_params, - EventFactory.HTTP_VERB, - EventFactory.HTTP_HEADERS) - - def test_create_conversion_event__with_user_agent_when_bot_filtering_is_enabled(self): - """ Test that create_conversion_event creates Event object + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + {'type': 'custom', 'value': 'test_value', 'entity_id': '111094', 'key': 'test_attribute'} + ], + 'snapshots': [ + { + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111095', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'test_event', + } + ] + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ): + event_obj = UserEventFactory.create_conversion_event( + self.project_config, 'test_event', 'test_user', {'test_attribute': 'test_value'}, None, + ) + + log_event = EventFactory.create_log_event(event_obj, self.logger) + + self._validate_event_object( + log_event, EventFactory.EVENT_ENDPOINT, expected_params, EventFactory.HTTP_VERB, EventFactory.HTTP_HEADERS, + ) + + def test_create_conversion_event__with_user_agent_when_bot_filtering_is_enabled(self,): + """ Test that create_conversion_event creates Event object with right params when user agent attribute is provided and bot filtering is enabled """ - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 'Edge', - 'entity_id': '$opt_user_agent', - 'key': '$opt_user_agent' - }, { - 'type': 'custom', - 'value': True, - 'entity_id': '$opt_bot_filtering', - 'key': '$opt_bot_filtering' - }], - 'snapshots': [{ - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111095', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'test_event' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ - mock.patch('optimizely.project_config.ProjectConfig.get_bot_filtering_value', return_value=True): - event_obj = UserEventFactory.create_conversion_event( - self.project_config, 'test_event', 'test_user', {'$opt_user_agent': 'Edge'}, None - ) - - log_event = EventFactory.create_log_event(event_obj, self.logger) - - self._validate_event_object(log_event, - EventFactory.EVENT_ENDPOINT, - expected_params, - EventFactory.HTTP_VERB, - EventFactory.HTTP_HEADERS) - - def test_create_conversion_event__with_user_agent_when_bot_filtering_is_disabled(self): - """ Test that create_conversion_event creates Event object + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + {'type': 'custom', 'value': 'Edge', 'entity_id': '$opt_user_agent', 'key': '$opt_user_agent'}, + { + 'type': 'custom', + 'value': True, + 'entity_id': '$opt_bot_filtering', + 'key': '$opt_bot_filtering', + }, + ], + 'snapshots': [ + { + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111095', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'test_event', + } + ] + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ), mock.patch( + 'optimizely.project_config.ProjectConfig.get_bot_filtering_value', return_value=True, + ): + event_obj = UserEventFactory.create_conversion_event( + self.project_config, 'test_event', 'test_user', {'$opt_user_agent': 'Edge'}, None, + ) + + log_event = EventFactory.create_log_event(event_obj, self.logger) + + self._validate_event_object( + log_event, EventFactory.EVENT_ENDPOINT, expected_params, EventFactory.HTTP_VERB, EventFactory.HTTP_HEADERS, + ) + + def test_create_conversion_event__with_user_agent_when_bot_filtering_is_disabled(self,): + """ Test that create_conversion_event creates Event object with right params when user agent attribute is provided and bot filtering is disabled """ - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 'Chrome', - 'entity_id': '$opt_user_agent', - 'key': '$opt_user_agent' - }, { - 'type': 'custom', - 'value': False, - 'entity_id': '$opt_bot_filtering', - 'key': '$opt_bot_filtering' - }], - 'snapshots': [{ - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111095', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'test_event' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ - mock.patch('optimizely.project_config.ProjectConfig.get_bot_filtering_value', return_value=False): - event_obj = UserEventFactory.create_conversion_event( - self.project_config, 'test_event', 'test_user', {'$opt_user_agent': 'Chrome'}, None - ) - - log_event = EventFactory.create_log_event(event_obj, self.logger) - - self._validate_event_object(log_event, - EventFactory.EVENT_ENDPOINT, - expected_params, - EventFactory.HTTP_VERB, - EventFactory.HTTP_HEADERS) - - def test_create_conversion_event__with_event_tags(self): - """ Test that create_conversion_event creates Event object + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + { + 'type': 'custom', + 'value': 'Chrome', + 'entity_id': '$opt_user_agent', + 'key': '$opt_user_agent', + }, + { + 'type': 'custom', + 'value': False, + 'entity_id': '$opt_bot_filtering', + 'key': '$opt_bot_filtering', + }, + ], + 'snapshots': [ + { + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111095', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'test_event', + } + ] + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ), mock.patch( + 'optimizely.project_config.ProjectConfig.get_bot_filtering_value', return_value=False, + ): + event_obj = UserEventFactory.create_conversion_event( + self.project_config, 'test_event', 'test_user', {'$opt_user_agent': 'Chrome'}, None, + ) + + log_event = EventFactory.create_log_event(event_obj, self.logger) + + self._validate_event_object( + log_event, EventFactory.EVENT_ENDPOINT, expected_params, EventFactory.HTTP_VERB, EventFactory.HTTP_HEADERS, + ) + + def test_create_conversion_event__with_event_tags(self): + """ Test that create_conversion_event creates Event object with right params when event tags are provided. """ - expected_params = { - 'client_version': version.__version__, - 'project_id': '111001', - 'visitors': [{ - 'attributes': [{ - 'entity_id': '111094', - 'type': 'custom', - 'value': 'test_value', - 'key': 'test_attribute' - }], - 'visitor_id': 'test_user', - 'snapshots': [{ - 'events': [{ - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'tags': { - 'non-revenue': 'abc', - 'revenue': 4200, - 'value': 1.234 - }, - 'timestamp': 42123, - 'revenue': 4200, - 'value': 1.234, - 'key': 'test_event', - 'entity_id': '111095' - }] - }] - }], - 'account_id': '12001', - 'client_name': 'python-sdk', - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'): - event_obj = UserEventFactory.create_conversion_event( - self.project_config, - 'test_event', - 'test_user', - {'test_attribute': 'test_value'}, - {'revenue': 4200, 'value': 1.234, 'non-revenue': 'abc'} - ) - - log_event = EventFactory.create_log_event(event_obj, self.logger) - - self._validate_event_object(log_event, - EventFactory.EVENT_ENDPOINT, - expected_params, - EventFactory.HTTP_VERB, - EventFactory.HTTP_HEADERS) - - def test_create_conversion_event__with_invalid_event_tags(self): - """ Test that create_conversion_event creates Event object + expected_params = { + 'client_version': version.__version__, + 'project_id': '111001', + 'visitors': [ + { + 'attributes': [ + {'entity_id': '111094', 'type': 'custom', 'value': 'test_value', 'key': 'test_attribute'} + ], + 'visitor_id': 'test_user', + 'snapshots': [ + { + 'events': [ + { + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'tags': {'non-revenue': 'abc', 'revenue': 4200, 'value': 1.234}, + 'timestamp': 42123, + 'revenue': 4200, + 'value': 1.234, + 'key': 'test_event', + 'entity_id': '111095', + } + ] + } + ], + } + ], + 'account_id': '12001', + 'client_name': 'python-sdk', + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ): + event_obj = UserEventFactory.create_conversion_event( + self.project_config, + 'test_event', + 'test_user', + {'test_attribute': 'test_value'}, + {'revenue': 4200, 'value': 1.234, 'non-revenue': 'abc'}, + ) + + log_event = EventFactory.create_log_event(event_obj, self.logger) + + self._validate_event_object( + log_event, EventFactory.EVENT_ENDPOINT, expected_params, EventFactory.HTTP_VERB, EventFactory.HTTP_HEADERS, + ) + + def test_create_conversion_event__with_invalid_event_tags(self): + """ Test that create_conversion_event creates Event object with right params when event tags are provided. """ - expected_params = { - 'client_version': version.__version__, - 'project_id': '111001', - 'visitors': [{ - 'attributes': [{ - 'entity_id': '111094', - 'type': 'custom', - 'value': 'test_value', - 'key': 'test_attribute' - }], - 'visitor_id': 'test_user', - 'snapshots': [{ - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111095', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'test_event', - 'tags': { - 'non-revenue': 'abc', - 'revenue': '4200', - 'value': True - } - }] - }] - }], - 'account_id': '12001', - 'client_name': 'python-sdk', - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'): - event_obj = UserEventFactory.create_conversion_event( - self.project_config, - 'test_event', - 'test_user', - {'test_attribute': 'test_value'}, - {'revenue': '4200', 'value': True, 'non-revenue': 'abc'} - ) - - log_event = EventFactory.create_log_event(event_obj, self.logger) - - self._validate_event_object(log_event, - EventFactory.EVENT_ENDPOINT, - expected_params, - EventFactory.HTTP_VERB, - EventFactory.HTTP_HEADERS) - - def test_create_conversion_event__when_event_is_used_in_multiple_experiments(self): - """ Test that create_conversion_event creates Event object with + expected_params = { + 'client_version': version.__version__, + 'project_id': '111001', + 'visitors': [ + { + 'attributes': [ + {'entity_id': '111094', 'type': 'custom', 'value': 'test_value', 'key': 'test_attribute'} + ], + 'visitor_id': 'test_user', + 'snapshots': [ + { + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111095', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'test_event', + 'tags': {'non-revenue': 'abc', 'revenue': '4200', 'value': True}, + } + ] + } + ], + } + ], + 'account_id': '12001', + 'client_name': 'python-sdk', + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ): + event_obj = UserEventFactory.create_conversion_event( + self.project_config, + 'test_event', + 'test_user', + {'test_attribute': 'test_value'}, + {'revenue': '4200', 'value': True, 'non-revenue': 'abc'}, + ) + + log_event = EventFactory.create_log_event(event_obj, self.logger) + + self._validate_event_object( + log_event, EventFactory.EVENT_ENDPOINT, expected_params, EventFactory.HTTP_VERB, EventFactory.HTTP_HEADERS, + ) + + def test_create_conversion_event__when_event_is_used_in_multiple_experiments(self): + """ Test that create_conversion_event creates Event object with right params when multiple experiments use the same event. """ - expected_params = { - 'client_version': version.__version__, - 'project_id': '111001', - 'visitors': [{ - 'attributes': [{ - 'entity_id': '111094', - 'type': 'custom', - 'value': 'test_value', - 'key': 'test_attribute' - }], - 'visitor_id': 'test_user', - 'snapshots': [{ - 'events': [{ - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'tags': { - 'non-revenue': 'abc', - 'revenue': 4200, - 'value': 1.234 - }, - 'timestamp': 42123, - 'revenue': 4200, - 'value': 1.234, - 'key': 'test_event', - 'entity_id': '111095' - }] - }] - }], - 'account_id': '12001', - 'client_name': 'python-sdk', - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - with mock.patch('time.time', return_value=42.123), \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'): - event_obj = UserEventFactory.create_conversion_event( - self.project_config, - 'test_event', - 'test_user', - {'test_attribute': 'test_value'}, - {'revenue': 4200, 'value': 1.234, 'non-revenue': 'abc'} - ) - - log_event = EventFactory.create_log_event(event_obj, self.logger) - - self._validate_event_object(log_event, - EventFactory.EVENT_ENDPOINT, - expected_params, - EventFactory.HTTP_VERB, - EventFactory.HTTP_HEADERS) + expected_params = { + 'client_version': version.__version__, + 'project_id': '111001', + 'visitors': [ + { + 'attributes': [ + {'entity_id': '111094', 'type': 'custom', 'value': 'test_value', 'key': 'test_attribute'} + ], + 'visitor_id': 'test_user', + 'snapshots': [ + { + 'events': [ + { + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'tags': {'non-revenue': 'abc', 'revenue': 4200, 'value': 1.234}, + 'timestamp': 42123, + 'revenue': 4200, + 'value': 1.234, + 'key': 'test_event', + 'entity_id': '111095', + } + ] + } + ], + } + ], + 'account_id': '12001', + 'client_name': 'python-sdk', + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + with mock.patch('time.time', return_value=42.123), mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ): + event_obj = UserEventFactory.create_conversion_event( + self.project_config, + 'test_event', + 'test_user', + {'test_attribute': 'test_value'}, + {'revenue': 4200, 'value': 1.234, 'non-revenue': 'abc'}, + ) + + log_event = EventFactory.create_log_event(event_obj, self.logger) + + self._validate_event_object( + log_event, EventFactory.EVENT_ENDPOINT, expected_params, EventFactory.HTTP_VERB, EventFactory.HTTP_HEADERS, + ) diff --git a/tests/test_event_payload.py b/tests/test_event_payload.py index 8e3e385b..e8cd6fbc 100644 --- a/tests/test_event_payload.py +++ b/tests/test_event_payload.py @@ -17,104 +17,103 @@ class EventPayloadTest(base.BaseTest): + def test_impression_event_equals_serialized_payload(self): + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + {'type': 'custom', 'value': 'test_value', 'entity_id': '111094', 'key': 'test_attribute'} + ], + 'snapshots': [ + { + 'decisions': [ + {'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'} + ], + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } - def test_impression_event_equals_serialized_payload(self): - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 'test_value', - 'entity_id': '111094', - 'key': 'test_attribute' - }], - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111129', - 'experiment_id': '111127', - 'campaign_id': '111182' - }], - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated' - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } + batch = payload.EventBatch('12001', '111001', '42', 'python-sdk', version.__version__, False, True) + visitor_attr = payload.VisitorAttribute('111094', 'test_attribute', 'custom', 'test_value') + event = payload.SnapshotEvent('111182', 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', 'campaign_activated', 42123,) + event_decision = payload.Decision('111182', '111127', '111129') - batch = payload.EventBatch('12001', '111001', '42', 'python-sdk', version.__version__, - False, True) - visitor_attr = payload.VisitorAttribute('111094', 'test_attribute', 'custom', 'test_value') - event = payload.SnapshotEvent('111182', 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', 'campaign_activated', - 42123) - event_decision = payload.Decision('111182', '111127', '111129') + snapshots = payload.Snapshot([event], [event_decision]) + user = payload.Visitor([snapshots], [visitor_attr], 'test_user') - snapshots = payload.Snapshot([event], [event_decision]) - user = payload.Visitor([snapshots], [visitor_attr], 'test_user') + batch.visitors = [user] - batch.visitors = [user] + self.assertEqual(batch, expected_params) - self.assertEqual(batch, expected_params) + def test_conversion_event_equals_serialized_payload(self): + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + {'type': 'custom', 'value': 'test_value', 'entity_id': '111094', 'key': 'test_attribute'}, + {'type': 'custom', 'value': 'test_value2', 'entity_id': '111095', 'key': 'test_attribute2'}, + ], + 'snapshots': [ + { + 'events': [ + { + 'timestamp': 42123, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + 'revenue': 4200, + 'tags': {'non-revenue': 'abc', 'revenue': 4200, 'value': 1.234}, + 'value': 1.234, + } + ] + } + ], + } + ], + 'client_name': 'python-sdk', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } - def test_conversion_event_equals_serialized_payload(self): - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 'test_value', - 'entity_id': '111094', - 'key': 'test_attribute' - }, { - 'type': 'custom', - 'value': 'test_value2', - 'entity_id': '111095', - 'key': 'test_attribute2' - }], - 'snapshots': [{ - 'events': [{ - 'timestamp': 42123, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated', - 'revenue': 4200, - 'tags': { - 'non-revenue': 'abc', - 'revenue': 4200, - 'value': 1.234 - }, - 'value': 1.234 - }] - }] - }], - 'client_name': 'python-sdk', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } + batch = payload.EventBatch('12001', '111001', '42', 'python-sdk', version.__version__, False, True) + visitor_attr_1 = payload.VisitorAttribute('111094', 'test_attribute', 'custom', 'test_value') + visitor_attr_2 = payload.VisitorAttribute('111095', 'test_attribute2', 'custom', 'test_value2') + event = payload.SnapshotEvent( + '111182', + 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'campaign_activated', + 42123, + 4200, + 1.234, + {'revenue': 4200, 'value': 1.234, 'non-revenue': 'abc'}, + ) - batch = payload.EventBatch('12001', '111001', '42', 'python-sdk', version.__version__, - False, True) - visitor_attr_1 = payload.VisitorAttribute('111094', 'test_attribute', 'custom', 'test_value') - visitor_attr_2 = payload.VisitorAttribute('111095', 'test_attribute2', 'custom', 'test_value2') - event = payload.SnapshotEvent('111182', 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', 'campaign_activated', - 42123, 4200, 1.234, {'revenue': 4200, 'value': 1.234, 'non-revenue': 'abc'}) + snapshots = payload.Snapshot([event]) + user = payload.Visitor([snapshots], [visitor_attr_1, visitor_attr_2], 'test_user') - snapshots = payload.Snapshot([event]) - user = payload.Visitor([snapshots], [visitor_attr_1, visitor_attr_2], 'test_user') + batch.visitors = [user] - batch.visitors = [user] - - self.assertEqual(batch, expected_params) + self.assertEqual(batch, expected_params) diff --git a/tests/test_event_processor.py b/tests/test_event_processor.py index b18205ec..e16032fe 100644 --- a/tests/test_event_processor.py +++ b/tests/test_event_processor.py @@ -17,7 +17,10 @@ from six.moves import queue from optimizely.event.payload import Decision, Visitor -from optimizely.event.event_processor import BatchEventProcessor, ForwardingEventProcessor +from optimizely.event.event_processor import ( + BatchEventProcessor, + ForwardingEventProcessor, +) from optimizely.event.event_factory import EventFactory from optimizely.event.log_event import LogEvent from optimizely.event.user_event_factory import UserEventFactory @@ -27,483 +30,477 @@ class CanonicalEvent(object): + def __init__(self, experiment_id, variation_id, event_name, visitor_id, attributes, tags): + self._experiment_id = experiment_id + self._variation_id = variation_id + self._event_name = event_name + self._visitor_id = visitor_id + self._attributes = attributes or {} + self._tags = tags or {} - def __init__(self, experiment_id, variation_id, event_name, visitor_id, attributes, tags): - self._experiment_id = experiment_id - self._variation_id = variation_id - self._event_name = event_name - self._visitor_id = visitor_id - self._attributes = attributes or {} - self._tags = tags or {} + def __eq__(self, other): + if other is None: + return False - def __eq__(self, other): - if other is None: - return False - - return self.__dict__ == other.__dict__ + return self.__dict__ == other.__dict__ class TestEventDispatcher(object): - IMPRESSION_EVENT_NAME = 'campaign_activated' + IMPRESSION_EVENT_NAME = 'campaign_activated' - def __init__(self, countdown_event=None): - self.countdown_event = countdown_event - self.expected_events = list() - self.actual_events = list() + def __init__(self, countdown_event=None): + self.countdown_event = countdown_event + self.expected_events = list() + self.actual_events = list() - def compare_events(self): - if len(self.expected_events) != len(self.actual_events): - return False + def compare_events(self): + if len(self.expected_events) != len(self.actual_events): + return False - for index, event in enumerate(self.expected_events): - expected_event = event - actual_event = self.actual_events[index] + for index, event in enumerate(self.expected_events): + expected_event = event + actual_event = self.actual_events[index] - if not expected_event == actual_event: - return False + if not expected_event == actual_event: + return False - return True + return True - def dispatch_event(self, actual_log_event): - visitors = [] - log_event_params = actual_log_event.params + def dispatch_event(self, actual_log_event): + visitors = [] + log_event_params = actual_log_event.params - if 'visitors' in log_event_params: + if 'visitors' in log_event_params: - for visitor in log_event_params['visitors']: - visitor_instance = Visitor(**visitor) - visitors.append(visitor_instance) + for visitor in log_event_params['visitors']: + visitor_instance = Visitor(**visitor) + visitors.append(visitor_instance) - if len(visitors) == 0: - return + if len(visitors) == 0: + return - for visitor in visitors: - for snapshot in visitor.snapshots: - decisions = snapshot.get('decisions') or [Decision(None, None, None)] - for decision in decisions: - for event in snapshot.get('events'): - attributes = visitor.attributes + for visitor in visitors: + for snapshot in visitor.snapshots: + decisions = snapshot.get('decisions') or [Decision(None, None, None)] + for decision in decisions: + for event in snapshot.get('events'): + attributes = visitor.attributes - self.actual_events.append(CanonicalEvent(decision.experiment_id, decision.variation_id, - event.get('key'), visitor.visitor_id, attributes, - event.get('event_tags'))) + self.actual_events.append( + CanonicalEvent( + decision.experiment_id, + decision.variation_id, + event.get('key'), + visitor.visitor_id, + attributes, + event.get('event_tags'), + ) + ) - def expect_impression(self, experiment_id, variation_id, user_id, attributes=None): - self._expect(experiment_id, variation_id, self.IMPRESSION_EVENT_NAME, user_id, None) + def expect_impression(self, experiment_id, variation_id, user_id, attributes=None): + self._expect(experiment_id, variation_id, self.IMPRESSION_EVENT_NAME, user_id, None) - def expect_conversion(self, event_name, user_id, attributes=None, event_tags=None): - self._expect(None, None, event_name, user_id, attributes, event_tags) + def expect_conversion(self, event_name, user_id, attributes=None, event_tags=None): + self._expect(None, None, event_name, user_id, attributes, event_tags) - def _expect(self, experiment_id, variation_id, event_name, visitor_id, attributes, tags): - expected_event = CanonicalEvent(experiment_id, variation_id, event_name, visitor_id, attributes, tags) - self.expected_events.append(expected_event) + def _expect(self, experiment_id, variation_id, event_name, visitor_id, attributes, tags): + expected_event = CanonicalEvent(experiment_id, variation_id, event_name, visitor_id, attributes, tags) + self.expected_events.append(expected_event) class BatchEventProcessorTest(base.BaseTest): - DEFAULT_QUEUE_CAPACITY = 1000 - MAX_BATCH_SIZE = 10 - MAX_DURATION_SEC = 1 - MAX_TIMEOUT_INTERVAL_SEC = 5 + DEFAULT_QUEUE_CAPACITY = 1000 + MAX_BATCH_SIZE = 10 + MAX_DURATION_SEC = 1 + MAX_TIMEOUT_INTERVAL_SEC = 5 - def setUp(self, *args, **kwargs): - base.BaseTest.setUp(self, 'config_dict_with_multiple_experiments') - self.test_user_id = 'test_user' - self.event_name = 'test_event' - self.event_queue = queue.Queue(maxsize=self.DEFAULT_QUEUE_CAPACITY) - self.optimizely.logger = SimpleLogger() - self.notification_center = self.optimizely.notification_center + def setUp(self, *args, **kwargs): + base.BaseTest.setUp(self, 'config_dict_with_multiple_experiments') + self.test_user_id = 'test_user' + self.event_name = 'test_event' + self.event_queue = queue.Queue(maxsize=self.DEFAULT_QUEUE_CAPACITY) + self.optimizely.logger = SimpleLogger() + self.notification_center = self.optimizely.notification_center - def tearDown(self): - self.event_processor.stop() + def tearDown(self): + self.event_processor.stop() - def _build_conversion_event(self, event_name, project_config=None): - config = project_config or self.project_config - return UserEventFactory.create_conversion_event(config, event_name, self.test_user_id, {}, {}) + def _build_conversion_event(self, event_name, project_config=None): + config = project_config or self.project_config + return UserEventFactory.create_conversion_event(config, event_name, self.test_user_id, {}, {}) - def _set_event_processor(self, event_dispatcher, logger): - self.event_processor = BatchEventProcessor( - event_dispatcher, - logger, - True, - self.event_queue, - self.MAX_BATCH_SIZE, - self.MAX_DURATION_SEC, - self.MAX_TIMEOUT_INTERVAL_SEC, - self.optimizely.notification_center - ) + def _set_event_processor(self, event_dispatcher, logger): + self.event_processor = BatchEventProcessor( + event_dispatcher, + logger, + True, + self.event_queue, + self.MAX_BATCH_SIZE, + self.MAX_DURATION_SEC, + self.MAX_TIMEOUT_INTERVAL_SEC, + self.optimizely.notification_center, + ) - def test_drain_on_stop(self): - event_dispatcher = TestEventDispatcher() + def test_drain_on_stop(self): + event_dispatcher = TestEventDispatcher() - with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: - self._set_event_processor(event_dispatcher, mock_config_logging) + with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: + self._set_event_processor(event_dispatcher, mock_config_logging) - user_event = self._build_conversion_event(self.event_name) - self.event_processor.process(user_event) - event_dispatcher.expect_conversion(self.event_name, self.test_user_id) + user_event = self._build_conversion_event(self.event_name) + self.event_processor.process(user_event) + event_dispatcher.expect_conversion(self.event_name, self.test_user_id) - time.sleep(5) + time.sleep(5) - self.assertStrictTrue(event_dispatcher.compare_events()) - self.assertEqual(0, self.event_processor.event_queue.qsize()) + self.assertStrictTrue(event_dispatcher.compare_events()) + self.assertEqual(0, self.event_processor.event_queue.qsize()) - def test_flush_on_max_timeout(self): - event_dispatcher = TestEventDispatcher() + def test_flush_on_max_timeout(self): + event_dispatcher = TestEventDispatcher() - with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: - self._set_event_processor(event_dispatcher, mock_config_logging) + with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: + self._set_event_processor(event_dispatcher, mock_config_logging) - user_event = self._build_conversion_event(self.event_name) - self.event_processor.process(user_event) - event_dispatcher.expect_conversion(self.event_name, self.test_user_id) + user_event = self._build_conversion_event(self.event_name) + self.event_processor.process(user_event) + event_dispatcher.expect_conversion(self.event_name, self.test_user_id) - time.sleep(3) + time.sleep(3) - self.assertStrictTrue(event_dispatcher.compare_events()) - self.assertEqual(0, self.event_processor.event_queue.qsize()) + self.assertStrictTrue(event_dispatcher.compare_events()) + self.assertEqual(0, self.event_processor.event_queue.qsize()) - def test_flush_max_batch_size(self): - event_dispatcher = TestEventDispatcher() + def test_flush_max_batch_size(self): + event_dispatcher = TestEventDispatcher() - with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: - self._set_event_processor(event_dispatcher, mock_config_logging) + with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: + self._set_event_processor(event_dispatcher, mock_config_logging) - for i in range(0, self.MAX_BATCH_SIZE): - user_event = self._build_conversion_event(self.event_name) - self.event_processor.process(user_event) - event_dispatcher.expect_conversion(self.event_name, self.test_user_id) + for i in range(0, self.MAX_BATCH_SIZE): + user_event = self._build_conversion_event(self.event_name) + self.event_processor.process(user_event) + event_dispatcher.expect_conversion(self.event_name, self.test_user_id) - time.sleep(1) + time.sleep(1) - self.assertStrictTrue(event_dispatcher.compare_events()) - self.assertEqual(0, self.event_processor.event_queue.qsize()) + self.assertStrictTrue(event_dispatcher.compare_events()) + self.assertEqual(0, self.event_processor.event_queue.qsize()) - def test_flush(self): - event_dispatcher = TestEventDispatcher() + def test_flush(self): + event_dispatcher = TestEventDispatcher() - with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: - self._set_event_processor(event_dispatcher, mock_config_logging) + with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: + self._set_event_processor(event_dispatcher, mock_config_logging) - user_event = self._build_conversion_event(self.event_name) - self.event_processor.process(user_event) - self.event_processor.flush() - event_dispatcher.expect_conversion(self.event_name, self.test_user_id) + user_event = self._build_conversion_event(self.event_name) + self.event_processor.process(user_event) + self.event_processor.flush() + event_dispatcher.expect_conversion(self.event_name, self.test_user_id) - self.event_processor.process(user_event) - self.event_processor.flush() - event_dispatcher.expect_conversion(self.event_name, self.test_user_id) + self.event_processor.process(user_event) + self.event_processor.flush() + event_dispatcher.expect_conversion(self.event_name, self.test_user_id) - time.sleep(3) + time.sleep(3) - self.assertStrictTrue(event_dispatcher.compare_events()) - self.assertEqual(0, self.event_processor.event_queue.qsize()) + self.assertStrictTrue(event_dispatcher.compare_events()) + self.assertEqual(0, self.event_processor.event_queue.qsize()) - def test_flush_on_mismatch_revision(self): - event_dispatcher = TestEventDispatcher() + def test_flush_on_mismatch_revision(self): + event_dispatcher = TestEventDispatcher() - with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: - self._set_event_processor(event_dispatcher, mock_config_logging) + with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: + self._set_event_processor(event_dispatcher, mock_config_logging) - self.project_config.revision = 1 - self.project_config.project_id = 'X' + self.project_config.revision = 1 + self.project_config.project_id = 'X' - user_event_1 = self._build_conversion_event(self.event_name, self.project_config) - self.event_processor.process(user_event_1) - event_dispatcher.expect_conversion(self.event_name, self.test_user_id) + user_event_1 = self._build_conversion_event(self.event_name, self.project_config) + self.event_processor.process(user_event_1) + event_dispatcher.expect_conversion(self.event_name, self.test_user_id) - self.project_config.revision = 2 - self.project_config.project_id = 'X' + self.project_config.revision = 2 + self.project_config.project_id = 'X' - user_event_2 = self._build_conversion_event(self.event_name, self.project_config) - self.event_processor.process(user_event_2) - event_dispatcher.expect_conversion(self.event_name, self.test_user_id) + user_event_2 = self._build_conversion_event(self.event_name, self.project_config) + self.event_processor.process(user_event_2) + event_dispatcher.expect_conversion(self.event_name, self.test_user_id) - time.sleep(3) + time.sleep(3) - self.assertStrictTrue(event_dispatcher.compare_events()) - self.assertEqual(0, self.event_processor.event_queue.qsize()) + self.assertStrictTrue(event_dispatcher.compare_events()) + self.assertEqual(0, self.event_processor.event_queue.qsize()) - def test_flush_on_mismatch_project_id(self): - event_dispatcher = TestEventDispatcher() + def test_flush_on_mismatch_project_id(self): + event_dispatcher = TestEventDispatcher() - with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: - self._set_event_processor(event_dispatcher, mock_config_logging) + with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: + self._set_event_processor(event_dispatcher, mock_config_logging) - self.project_config.revision = 1 - self.project_config.project_id = 'X' + self.project_config.revision = 1 + self.project_config.project_id = 'X' - user_event_1 = self._build_conversion_event(self.event_name, self.project_config) - self.event_processor.process(user_event_1) - event_dispatcher.expect_conversion(self.event_name, self.test_user_id) + user_event_1 = self._build_conversion_event(self.event_name, self.project_config) + self.event_processor.process(user_event_1) + event_dispatcher.expect_conversion(self.event_name, self.test_user_id) - self.project_config.revision = 1 - self.project_config.project_id = 'Y' + self.project_config.revision = 1 + self.project_config.project_id = 'Y' - user_event_2 = self._build_conversion_event(self.event_name, self.project_config) - self.event_processor.process(user_event_2) - event_dispatcher.expect_conversion(self.event_name, self.test_user_id) + user_event_2 = self._build_conversion_event(self.event_name, self.project_config) + self.event_processor.process(user_event_2) + event_dispatcher.expect_conversion(self.event_name, self.test_user_id) - time.sleep(3) + time.sleep(3) - self.assertStrictTrue(event_dispatcher.compare_events()) - self.assertEqual(0, self.event_processor.event_queue.qsize()) + self.assertStrictTrue(event_dispatcher.compare_events()) + self.assertEqual(0, self.event_processor.event_queue.qsize()) - def test_stop_and_start(self): - event_dispatcher = TestEventDispatcher() + def test_stop_and_start(self): + event_dispatcher = TestEventDispatcher() - with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: - self._set_event_processor(event_dispatcher, mock_config_logging) + with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: + self._set_event_processor(event_dispatcher, mock_config_logging) - user_event = self._build_conversion_event(self.event_name, self.project_config) - self.event_processor.process(user_event) - event_dispatcher.expect_conversion(self.event_name, self.test_user_id) + user_event = self._build_conversion_event(self.event_name, self.project_config) + self.event_processor.process(user_event) + event_dispatcher.expect_conversion(self.event_name, self.test_user_id) - time.sleep(3) + time.sleep(3) - self.assertStrictTrue(event_dispatcher.compare_events()) - self.event_processor.stop() + self.assertStrictTrue(event_dispatcher.compare_events()) + self.event_processor.stop() - self.event_processor.process(user_event) - event_dispatcher.expect_conversion(self.event_name, self.test_user_id) + self.event_processor.process(user_event) + event_dispatcher.expect_conversion(self.event_name, self.test_user_id) - self.event_processor.start() - self.assertStrictTrue(self.event_processor.is_running) + self.event_processor.start() + self.assertStrictTrue(self.event_processor.is_running) - self.event_processor.stop() - self.assertStrictFalse(self.event_processor.is_running) + self.event_processor.stop() + self.assertStrictFalse(self.event_processor.is_running) - self.assertEqual(0, self.event_processor.event_queue.qsize()) - - def test_init__invalid_batch_size(self): - event_dispatcher = TestEventDispatcher() - - with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: - self.event_processor = BatchEventProcessor( - event_dispatcher, - self.optimizely.logger, - True, - self.event_queue, - 5.5, - self.MAX_DURATION_SEC, - self.MAX_TIMEOUT_INTERVAL_SEC - ) - - # default batch size is 10. - self.assertEqual(10, self.event_processor.batch_size) - mock_config_logging.info.assert_called_with('Using default value 10 for batch_size.') - - def test_init__NaN_batch_size(self): - event_dispatcher = TestEventDispatcher() - - with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: - self.event_processor = BatchEventProcessor( - event_dispatcher, - self.optimizely.logger, - True, - self.event_queue, - 'batch_size', - self.MAX_DURATION_SEC, - self.MAX_TIMEOUT_INTERVAL_SEC - ) - - # default batch size is 10. - self.assertEqual(10, self.event_processor.batch_size) - mock_config_logging.info.assert_called_with('Using default value 10 for batch_size.') - - def test_init__invalid_flush_interval(self): - event_dispatcher = TestEventDispatcher() - - with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: - self.event_processor = BatchEventProcessor( - event_dispatcher, - mock_config_logging, - True, - self.event_queue, - self.MAX_BATCH_SIZE, - 0, - self.MAX_TIMEOUT_INTERVAL_SEC - ) - - # default flush interval is 30s. - self.assertEqual(datetime.timedelta(seconds=30), self.event_processor.flush_interval) - mock_config_logging.info.assert_called_with('Using default value 30 for flush_interval.') - - def test_init__bool_flush_interval(self): - event_dispatcher = TestEventDispatcher() - - with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: - self.event_processor = BatchEventProcessor( - event_dispatcher, - self.optimizely.logger, - True, - self.event_queue, - self.MAX_BATCH_SIZE, - True, - self.MAX_TIMEOUT_INTERVAL_SEC - ) - - # default flush interval is 30s. - self.assertEqual(datetime.timedelta(seconds=30), self.event_processor.flush_interval) - mock_config_logging.info.assert_called_with('Using default value 30 for flush_interval.') - - def test_init__string_flush_interval(self): - event_dispatcher = TestEventDispatcher() - - with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: - self.event_processor = BatchEventProcessor( - event_dispatcher, - self.optimizely.logger, - True, - self.event_queue, - self.MAX_BATCH_SIZE, - 'True', - self.MAX_TIMEOUT_INTERVAL_SEC - ) - - # default flush interval is 30s. - self.assertEqual(datetime.timedelta(seconds=30), self.event_processor.flush_interval) - mock_config_logging.info.assert_called_with('Using default value 30 for flush_interval.') - - def test_init__invalid_timeout_interval(self): - event_dispatcher = TestEventDispatcher() - - with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: - self.event_processor = BatchEventProcessor( - event_dispatcher, - self.optimizely.logger, - True, - self.event_queue, - self.MAX_BATCH_SIZE, - self.MAX_DURATION_SEC, - -100 - ) - - # default timeout interval is 5s. - self.assertEqual(datetime.timedelta(seconds=5), self.event_processor.timeout_interval) - mock_config_logging.info.assert_called_with('Using default value 5 for timeout_interval.') - - def test_init__NaN_timeout_interval(self): - event_dispatcher = TestEventDispatcher() - - with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: - self.event_processor = BatchEventProcessor( - event_dispatcher, - self.optimizely.logger, - True, - self.event_queue, - self.MAX_BATCH_SIZE, - self.MAX_DURATION_SEC, - False - ) - - # default timeout interval is 5s. - self.assertEqual(datetime.timedelta(seconds=5), self.event_processor.timeout_interval) - mock_config_logging.info.assert_called_with('Using default value 5 for timeout_interval.') - - def test_notification_center__on_log_event(self): - - mock_event_dispatcher = mock.Mock() - callback_hit = [False] - - def on_log_event(log_event): - self.assertStrictTrue(isinstance(log_event, LogEvent)) - callback_hit[0] = True - - self.optimizely.notification_center.add_notification_listener( - enums.NotificationTypes.LOG_EVENT, on_log_event - ) - - with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: - self._set_event_processor(mock_event_dispatcher, mock_config_logging) - - user_event = self._build_conversion_event(self.event_name, self.project_config) - self.event_processor.process(user_event) - - self.event_processor.stop() - - self.assertEqual(True, callback_hit[0]) - self.assertEqual(1, len(self.optimizely.notification_center.notification_listeners[ - enums.NotificationTypes.LOG_EVENT - ])) + self.assertEqual(0, self.event_processor.event_queue.qsize()) + + def test_init__invalid_batch_size(self): + event_dispatcher = TestEventDispatcher() + + with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: + self.event_processor = BatchEventProcessor( + event_dispatcher, + self.optimizely.logger, + True, + self.event_queue, + 5.5, + self.MAX_DURATION_SEC, + self.MAX_TIMEOUT_INTERVAL_SEC, + ) + + # default batch size is 10. + self.assertEqual(10, self.event_processor.batch_size) + mock_config_logging.info.assert_called_with('Using default value 10 for batch_size.') + + def test_init__NaN_batch_size(self): + event_dispatcher = TestEventDispatcher() + + with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: + self.event_processor = BatchEventProcessor( + event_dispatcher, + self.optimizely.logger, + True, + self.event_queue, + 'batch_size', + self.MAX_DURATION_SEC, + self.MAX_TIMEOUT_INTERVAL_SEC, + ) + + # default batch size is 10. + self.assertEqual(10, self.event_processor.batch_size) + mock_config_logging.info.assert_called_with('Using default value 10 for batch_size.') + + def test_init__invalid_flush_interval(self): + event_dispatcher = TestEventDispatcher() + + with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: + self.event_processor = BatchEventProcessor( + event_dispatcher, + mock_config_logging, + True, + self.event_queue, + self.MAX_BATCH_SIZE, + 0, + self.MAX_TIMEOUT_INTERVAL_SEC, + ) + + # default flush interval is 30s. + self.assertEqual(datetime.timedelta(seconds=30), self.event_processor.flush_interval) + mock_config_logging.info.assert_called_with('Using default value 30 for flush_interval.') + + def test_init__bool_flush_interval(self): + event_dispatcher = TestEventDispatcher() + + with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: + self.event_processor = BatchEventProcessor( + event_dispatcher, + self.optimizely.logger, + True, + self.event_queue, + self.MAX_BATCH_SIZE, + True, + self.MAX_TIMEOUT_INTERVAL_SEC, + ) + + # default flush interval is 30s. + self.assertEqual(datetime.timedelta(seconds=30), self.event_processor.flush_interval) + mock_config_logging.info.assert_called_with('Using default value 30 for flush_interval.') + + def test_init__string_flush_interval(self): + event_dispatcher = TestEventDispatcher() + + with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: + self.event_processor = BatchEventProcessor( + event_dispatcher, + self.optimizely.logger, + True, + self.event_queue, + self.MAX_BATCH_SIZE, + 'True', + self.MAX_TIMEOUT_INTERVAL_SEC, + ) + + # default flush interval is 30s. + self.assertEqual(datetime.timedelta(seconds=30), self.event_processor.flush_interval) + mock_config_logging.info.assert_called_with('Using default value 30 for flush_interval.') + + def test_init__invalid_timeout_interval(self): + event_dispatcher = TestEventDispatcher() + + with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: + self.event_processor = BatchEventProcessor( + event_dispatcher, + self.optimizely.logger, + True, + self.event_queue, + self.MAX_BATCH_SIZE, + self.MAX_DURATION_SEC, + -100, + ) + + # default timeout interval is 5s. + self.assertEqual(datetime.timedelta(seconds=5), self.event_processor.timeout_interval) + mock_config_logging.info.assert_called_with('Using default value 5 for timeout_interval.') + + def test_init__NaN_timeout_interval(self): + event_dispatcher = TestEventDispatcher() + + with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: + self.event_processor = BatchEventProcessor( + event_dispatcher, + self.optimizely.logger, + True, + self.event_queue, + self.MAX_BATCH_SIZE, + self.MAX_DURATION_SEC, + False, + ) + + # default timeout interval is 5s. + self.assertEqual(datetime.timedelta(seconds=5), self.event_processor.timeout_interval) + mock_config_logging.info.assert_called_with('Using default value 5 for timeout_interval.') + + def test_notification_center__on_log_event(self): + + mock_event_dispatcher = mock.Mock() + callback_hit = [False] + + def on_log_event(log_event): + self.assertStrictTrue(isinstance(log_event, LogEvent)) + callback_hit[0] = True + + self.optimizely.notification_center.add_notification_listener(enums.NotificationTypes.LOG_EVENT, on_log_event) + + with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: + self._set_event_processor(mock_event_dispatcher, mock_config_logging) + + user_event = self._build_conversion_event(self.event_name, self.project_config) + self.event_processor.process(user_event) + + self.event_processor.stop() + + self.assertEqual(True, callback_hit[0]) + self.assertEqual( + 1, len(self.optimizely.notification_center.notification_listeners[enums.NotificationTypes.LOG_EVENT]), + ) class TestForwardingEventDispatcher(object): + def __init__(self, is_updated=False): + self.is_updated = is_updated - def __init__(self, is_updated=False): - self.is_updated = is_updated - - def dispatch_event(self, log_event): - if log_event.http_verb == 'POST' and log_event.url == EventFactory.EVENT_ENDPOINT: - self.is_updated = True - return self.is_updated + def dispatch_event(self, log_event): + if log_event.http_verb == 'POST' and log_event.url == EventFactory.EVENT_ENDPOINT: + self.is_updated = True + return self.is_updated class ForwardingEventProcessorTest(base.BaseTest): + def setUp(self, *args, **kwargs): + base.BaseTest.setUp(self, 'config_dict_with_multiple_experiments') + self.test_user_id = 'test_user' + self.event_name = 'test_event' + self.optimizely.logger = SimpleLogger() + self.notification_center = self.optimizely.notification_center + self.event_dispatcher = TestForwardingEventDispatcher(is_updated=False) + + with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: + self.event_processor = ForwardingEventProcessor( + self.event_dispatcher, mock_config_logging, self.notification_center + ) + + def _build_conversion_event(self, event_name): + return UserEventFactory.create_conversion_event(self.project_config, event_name, self.test_user_id, {}, {}) + + def test_event_processor__dispatch_raises_exception(self): + """ Test that process logs dispatch failure gracefully. """ + + user_event = self._build_conversion_event(self.event_name) + log_event = EventFactory.create_log_event(user_event, self.optimizely.logger) + + with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, mock.patch.object( + self.event_dispatcher, 'dispatch_event', side_effect=Exception('Failed to send.'), + ): + + event_processor = ForwardingEventProcessor( + self.event_dispatcher, mock_client_logging, self.notification_center + ) + event_processor.process(user_event) + + mock_client_logging.exception.assert_called_once_with( + 'Error dispatching event: ' + str(log_event) + ' Failed to send.' + ) + + def test_event_processor__with_test_event_dispatcher(self): + user_event = self._build_conversion_event(self.event_name) + self.event_processor.process(user_event) + self.assertStrictTrue(self.event_dispatcher.is_updated) + + def test_notification_center(self): + + callback_hit = [False] + + def on_log_event(log_event): + self.assertStrictTrue(isinstance(log_event, LogEvent)) + callback_hit[0] = True + + self.optimizely.notification_center.add_notification_listener(enums.NotificationTypes.LOG_EVENT, on_log_event) + + user_event = self._build_conversion_event(self.event_name) + self.event_processor.process(user_event) - def setUp(self, *args, **kwargs): - base.BaseTest.setUp(self, 'config_dict_with_multiple_experiments') - self.test_user_id = 'test_user' - self.event_name = 'test_event' - self.optimizely.logger = SimpleLogger() - self.notification_center = self.optimizely.notification_center - self.event_dispatcher = TestForwardingEventDispatcher(is_updated=False) - - with mock.patch.object(self.optimizely, 'logger') as mock_config_logging: - self.event_processor = ForwardingEventProcessor( - self.event_dispatcher, - mock_config_logging, - self.notification_center - ) - - def _build_conversion_event(self, event_name): - return UserEventFactory.create_conversion_event( - self.project_config, - event_name, - self.test_user_id, - {}, - {} - ) - - def test_event_processor__dispatch_raises_exception(self): - """ Test that process logs dispatch failure gracefully. """ - - user_event = self._build_conversion_event(self.event_name) - log_event = EventFactory.create_log_event(user_event, self.optimizely.logger) - - with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, \ - mock.patch.object(self.event_dispatcher, 'dispatch_event', - side_effect=Exception('Failed to send.')): - - event_processor = ForwardingEventProcessor(self.event_dispatcher, mock_client_logging, self.notification_center) - event_processor.process(user_event) - - mock_client_logging.exception.assert_called_once_with( - 'Error dispatching event: ' + str(log_event) + ' Failed to send.' - ) - - def test_event_processor__with_test_event_dispatcher(self): - user_event = self._build_conversion_event(self.event_name) - self.event_processor.process(user_event) - self.assertStrictTrue(self.event_dispatcher.is_updated) - - def test_notification_center(self): - - callback_hit = [False] - - def on_log_event(log_event): - self.assertStrictTrue(isinstance(log_event, LogEvent)) - callback_hit[0] = True - - self.optimizely.notification_center.add_notification_listener( - enums.NotificationTypes.LOG_EVENT, on_log_event - ) - - user_event = self._build_conversion_event(self.event_name) - self.event_processor.process(user_event) - - self.assertEqual(True, callback_hit[0]) - self.assertEqual(1, len(self.optimizely.notification_center.notification_listeners[ - enums.NotificationTypes.LOG_EVENT - ])) + self.assertEqual(True, callback_hit[0]) + self.assertEqual( + 1, len(self.optimizely.notification_center.notification_listeners[enums.NotificationTypes.LOG_EVENT]), + ) diff --git a/tests/test_logger.py b/tests/test_logger.py index fcfb72f8..64cd1378 100644 --- a/tests/test_logger.py +++ b/tests/test_logger.py @@ -20,128 +20,119 @@ class SimpleLoggerTests(unittest.TestCase): + def test_log__deprecation_warning(self): + """Test that SimpleLogger now outputs a deprecation warning on ``.log`` calls.""" + simple_logger = _logger.SimpleLogger() + actual_log_patch = mock.patch.object(simple_logger, 'logger') + warnings_patch = mock.patch('warnings.warn') + with warnings_patch as patched_warnings, actual_log_patch as log_patch: + simple_logger.log(logging.INFO, 'Message') - def test_log__deprecation_warning(self): - """Test that SimpleLogger now outputs a deprecation warning on ``.log`` calls.""" - simple_logger = _logger.SimpleLogger() - actual_log_patch = mock.patch.object(simple_logger, 'logger') - warnings_patch = mock.patch('warnings.warn') - with warnings_patch as patched_warnings, actual_log_patch as log_patch: - simple_logger.log(logging.INFO, 'Message') - - msg = " is deprecated. " \ - "Please use standard python loggers." - patched_warnings.assert_called_once_with(msg, DeprecationWarning) - log_patch.log.assert_called_once_with(logging.INFO, 'Message') + msg = " is deprecated. " "Please use standard python loggers." + patched_warnings.assert_called_once_with(msg, DeprecationWarning) + log_patch.log.assert_called_once_with(logging.INFO, 'Message') class AdaptLoggerTests(unittest.TestCase): - - def test_adapt_logger__standard_logger(self): - """Test that adapt_logger does nothing to standard python loggers.""" - logger_name = str(uuid.uuid4()) - standard_logger = logging.getLogger(logger_name) - adapted = _logger.adapt_logger(standard_logger) - self.assertIs(standard_logger, adapted) - - def test_adapt_logger__simple(self): - """Test that adapt_logger returns a standard python logger from a SimpleLogger.""" - simple_logger = _logger.SimpleLogger() - standard_logger = _logger.adapt_logger(simple_logger) - - # adapt_logger knows about the loggers attached to this class. - self.assertIs(simple_logger.logger, standard_logger) - - # Verify the standard properties of the logger. - self.assertIsInstance(standard_logger, logging.Logger) - self.assertEqual('optimizely.logger.SimpleLogger', standard_logger.name) - self.assertEqual(logging.INFO, standard_logger.level) - - # Should have a single StreamHandler with our default formatting. - self.assertEqual(1, len(standard_logger.handlers)) - handler = standard_logger.handlers[0] - self.assertIsInstance(handler, logging.StreamHandler) - self.assertEqual( - '%(levelname)-8s %(asctime)s %(filename)s:%(lineno)s:%(message)s', - handler.formatter._fmt - ) - - def test_adapt_logger__noop(self): - """Test that adapt_logger returns a standard python logger from a NoOpLogger.""" - noop_logger = _logger.NoOpLogger() - standard_logger = _logger.adapt_logger(noop_logger) - - # adapt_logger knows about the loggers attached to this class. - self.assertIs(noop_logger.logger, standard_logger) - - # Verify properties of the logger - self.assertIsInstance(standard_logger, logging.Logger) - self.assertEqual('optimizely.logger.NoOpLogger', standard_logger.name) - self.assertEqual(logging.NOTSET, standard_logger.level) - - # Should have a single NullHandler (with a default formatter). - self.assertEqual(1, len(standard_logger.handlers)) - handler = standard_logger.handlers[0] - self.assertIsInstance(handler, logging.NullHandler) - self.assertEqual( - '%(levelname)-8s %(asctime)s %(filename)s:%(lineno)s:%(message)s', - handler.formatter._fmt - ) - - def test_adapt_logger__unknown(self): - """Test that adapt_logger gives back things it can't adapt.""" - obj = object() - value = _logger.adapt_logger(obj) - self.assertIs(obj, value) + def test_adapt_logger__standard_logger(self): + """Test that adapt_logger does nothing to standard python loggers.""" + logger_name = str(uuid.uuid4()) + standard_logger = logging.getLogger(logger_name) + adapted = _logger.adapt_logger(standard_logger) + self.assertIs(standard_logger, adapted) + + def test_adapt_logger__simple(self): + """Test that adapt_logger returns a standard python logger from a SimpleLogger.""" + simple_logger = _logger.SimpleLogger() + standard_logger = _logger.adapt_logger(simple_logger) + + # adapt_logger knows about the loggers attached to this class. + self.assertIs(simple_logger.logger, standard_logger) + + # Verify the standard properties of the logger. + self.assertIsInstance(standard_logger, logging.Logger) + self.assertEqual('optimizely.logger.SimpleLogger', standard_logger.name) + self.assertEqual(logging.INFO, standard_logger.level) + + # Should have a single StreamHandler with our default formatting. + self.assertEqual(1, len(standard_logger.handlers)) + handler = standard_logger.handlers[0] + self.assertIsInstance(handler, logging.StreamHandler) + self.assertEqual( + '%(levelname)-8s %(asctime)s %(filename)s:%(lineno)s:%(message)s', handler.formatter._fmt, + ) + + def test_adapt_logger__noop(self): + """Test that adapt_logger returns a standard python logger from a NoOpLogger.""" + noop_logger = _logger.NoOpLogger() + standard_logger = _logger.adapt_logger(noop_logger) + + # adapt_logger knows about the loggers attached to this class. + self.assertIs(noop_logger.logger, standard_logger) + + # Verify properties of the logger + self.assertIsInstance(standard_logger, logging.Logger) + self.assertEqual('optimizely.logger.NoOpLogger', standard_logger.name) + self.assertEqual(logging.NOTSET, standard_logger.level) + + # Should have a single NullHandler (with a default formatter). + self.assertEqual(1, len(standard_logger.handlers)) + handler = standard_logger.handlers[0] + self.assertIsInstance(handler, logging.NullHandler) + self.assertEqual( + '%(levelname)-8s %(asctime)s %(filename)s:%(lineno)s:%(message)s', handler.formatter._fmt, + ) + + def test_adapt_logger__unknown(self): + """Test that adapt_logger gives back things it can't adapt.""" + obj = object() + value = _logger.adapt_logger(obj) + self.assertIs(obj, value) class GetLoggerTests(unittest.TestCase): - - def test_reset_logger(self): - """Test that reset_logger gives back a standard python logger with defaults.""" - logger_name = str(uuid.uuid4()) - logger = _logger.reset_logger(logger_name) - self.assertEqual(logger_name, logger.name) - self.assertEqual(1, len(logger.handlers)) - handler = logger.handlers[0] - self.assertIsInstance(handler, logging.StreamHandler) - self.assertEqual( - '%(levelname)-8s %(asctime)s %(filename)s:%(lineno)s:%(message)s', - handler.formatter._fmt - ) - - def test_reset_logger__replaces_handlers(self): - """Test that reset_logger replaces existing handlers with a StreamHandler.""" - logger_name = 'test-logger-{}'.format(uuid.uuid4()) - logger = logging.getLogger(logger_name) - logger.handlers = [logging.StreamHandler() for _ in range(10)] - - reset_logger = _logger.reset_logger(logger_name) - self.assertEqual(1, len(reset_logger.handlers)) - - handler = reset_logger.handlers[0] - self.assertIsInstance(handler, logging.StreamHandler) - self.assertEqual( - '%(levelname)-8s %(asctime)s %(filename)s:%(lineno)s:%(message)s', - handler.formatter._fmt - ) - - def test_reset_logger__with_handler__existing(self): - """Test that reset_logger deals with provided handlers correctly.""" - existing_handler = logging.NullHandler() - logger_name = 'test-logger-{}'.format(uuid.uuid4()) - reset_logger = _logger.reset_logger(logger_name, handler=existing_handler) - self.assertEqual(1, len(reset_logger.handlers)) - - handler = reset_logger.handlers[0] - self.assertIs(existing_handler, handler) - self.assertEqual( - '%(levelname)-8s %(asctime)s %(filename)s:%(lineno)s:%(message)s', - handler.formatter._fmt - ) - - def test_reset_logger__with_level(self): - """Test that reset_logger sets log levels correctly.""" - logger_name = 'test-logger-{}'.format(uuid.uuid4()) - reset_logger = _logger.reset_logger(logger_name, level=logging.DEBUG) - self.assertEqual(logging.DEBUG, reset_logger.level) + def test_reset_logger(self): + """Test that reset_logger gives back a standard python logger with defaults.""" + logger_name = str(uuid.uuid4()) + logger = _logger.reset_logger(logger_name) + self.assertEqual(logger_name, logger.name) + self.assertEqual(1, len(logger.handlers)) + handler = logger.handlers[0] + self.assertIsInstance(handler, logging.StreamHandler) + self.assertEqual( + '%(levelname)-8s %(asctime)s %(filename)s:%(lineno)s:%(message)s', handler.formatter._fmt, + ) + + def test_reset_logger__replaces_handlers(self): + """Test that reset_logger replaces existing handlers with a StreamHandler.""" + logger_name = 'test-logger-{}'.format(uuid.uuid4()) + logger = logging.getLogger(logger_name) + logger.handlers = [logging.StreamHandler() for _ in range(10)] + + reset_logger = _logger.reset_logger(logger_name) + self.assertEqual(1, len(reset_logger.handlers)) + + handler = reset_logger.handlers[0] + self.assertIsInstance(handler, logging.StreamHandler) + self.assertEqual( + '%(levelname)-8s %(asctime)s %(filename)s:%(lineno)s:%(message)s', handler.formatter._fmt, + ) + + def test_reset_logger__with_handler__existing(self): + """Test that reset_logger deals with provided handlers correctly.""" + existing_handler = logging.NullHandler() + logger_name = 'test-logger-{}'.format(uuid.uuid4()) + reset_logger = _logger.reset_logger(logger_name, handler=existing_handler) + self.assertEqual(1, len(reset_logger.handlers)) + + handler = reset_logger.handlers[0] + self.assertIs(existing_handler, handler) + self.assertEqual( + '%(levelname)-8s %(asctime)s %(filename)s:%(lineno)s:%(message)s', handler.formatter._fmt, + ) + + def test_reset_logger__with_level(self): + """Test that reset_logger sets log levels correctly.""" + logger_name = 'test-logger-{}'.format(uuid.uuid4()) + reset_logger = _logger.reset_logger(logger_name, level=logging.DEBUG) + self.assertEqual(logging.DEBUG, reset_logger.level) diff --git a/tests/test_notification_center.py b/tests/test_notification_center.py index 4ed8ba0d..2ac30903 100644 --- a/tests/test_notification_center.py +++ b/tests/test_notification_center.py @@ -39,7 +39,6 @@ def on_log_event_listener(*args): class NotificationCenterTest(unittest.TestCase): - def test_add_notification_listener__valid_type(self): """ Test successfully adding a notification listener. """ @@ -48,24 +47,27 @@ def test_add_notification_listener__valid_type(self): # Test by adding different supported notification listeners. self.assertEqual( 1, - test_notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, on_activate_listener) + test_notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, on_activate_listener), ) self.assertEqual( 2, - test_notification_center.add_notification_listener(enums.NotificationTypes.OPTIMIZELY_CONFIG_UPDATE, - on_config_update_listener) + test_notification_center.add_notification_listener( + enums.NotificationTypes.OPTIMIZELY_CONFIG_UPDATE, on_config_update_listener, + ), ) self.assertEqual( 3, - test_notification_center.add_notification_listener(enums.NotificationTypes.DECISION, on_decision_listener) + test_notification_center.add_notification_listener(enums.NotificationTypes.DECISION, on_decision_listener), ) self.assertEqual( - 4, test_notification_center.add_notification_listener(enums.NotificationTypes.TRACK, on_track_listener) + 4, test_notification_center.add_notification_listener(enums.NotificationTypes.TRACK, on_track_listener), ) self.assertEqual( - 5, test_notification_center.add_notification_listener(enums.NotificationTypes.LOG_EVENT, - on_log_event_listener) + 5, + test_notification_center.add_notification_listener( + enums.NotificationTypes.LOG_EVENT, on_log_event_listener + ), ) def test_add_notification_listener__multiple_listeners(self): @@ -79,11 +81,13 @@ def another_on_activate_listener(*args): # Test by adding multiple listeners of same type. self.assertEqual( 1, - test_notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, on_activate_listener) + test_notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, on_activate_listener), ) self.assertEqual( - 2, test_notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, - another_on_activate_listener) + 2, + test_notification_center.add_notification_listener( + enums.NotificationTypes.ACTIVATE, another_on_activate_listener + ), ) def test_add_notification_listener__invalid_type(self): @@ -96,11 +100,11 @@ def notif_listener(*args): pass self.assertEqual( - -1, - test_notification_center.add_notification_listener('invalid_notification_type', notif_listener) + -1, test_notification_center.add_notification_listener('invalid_notification_type', notif_listener), + ) + mock_logger.error.assert_called_once_with( + 'Invalid notification_type: invalid_notification_type provided. ' 'Not adding listener.' ) - mock_logger.error.assert_called_once_with('Invalid notification_type: invalid_notification_type provided. ' - 'Not adding listener.') def test_add_notification_listener__same_listener(self): """ Test that adding same listener again does nothing and returns -1. """ @@ -109,17 +113,19 @@ def test_add_notification_listener__same_listener(self): test_notification_center = notification_center.NotificationCenter(logger=mock_logger) self.assertEqual( - 1, - test_notification_center.add_notification_listener(enums.NotificationTypes.TRACK, on_track_listener) + 1, test_notification_center.add_notification_listener(enums.NotificationTypes.TRACK, on_track_listener), + ) + self.assertEqual( + 1, len(test_notification_center.notification_listeners[enums.NotificationTypes.TRACK]), ) - self.assertEqual(1, len(test_notification_center.notification_listeners[enums.NotificationTypes.TRACK])) # Test that adding same listener again makes no difference. self.assertEqual( - -1, - test_notification_center.add_notification_listener(enums.NotificationTypes.TRACK, on_track_listener) + -1, test_notification_center.add_notification_listener(enums.NotificationTypes.TRACK, on_track_listener), + ) + self.assertEqual( + 1, len(test_notification_center.notification_listeners[enums.NotificationTypes.TRACK]), ) - self.assertEqual(1, len(test_notification_center.notification_listeners[enums.NotificationTypes.TRACK])) mock_logger.error.assert_called_once_with('Listener has already been added. Not adding it again.') def test_remove_notification_listener__valid_listener(self): @@ -133,25 +139,37 @@ def another_on_activate_listener(*args): # Add multiple notification listeners. self.assertEqual( 1, - test_notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, on_activate_listener) + test_notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, on_activate_listener), ) self.assertEqual( 2, - test_notification_center.add_notification_listener(enums.NotificationTypes.DECISION, on_decision_listener) + test_notification_center.add_notification_listener(enums.NotificationTypes.DECISION, on_decision_listener), ) self.assertEqual( - 3, test_notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, - another_on_activate_listener) + 3, + test_notification_center.add_notification_listener( + enums.NotificationTypes.ACTIVATE, another_on_activate_listener + ), ) - self.assertEqual(2, len(test_notification_center.notification_listeners[enums.NotificationTypes.ACTIVATE])) - self.assertEqual(1, len(test_notification_center.notification_listeners[enums.NotificationTypes.DECISION])) - self.assertEqual(0, len(test_notification_center.notification_listeners[enums.NotificationTypes.TRACK])) - self.assertEqual(0, len(test_notification_center.notification_listeners[enums.NotificationTypes.LOG_EVENT])) + self.assertEqual( + 2, len(test_notification_center.notification_listeners[enums.NotificationTypes.ACTIVATE]), + ) + self.assertEqual( + 1, len(test_notification_center.notification_listeners[enums.NotificationTypes.DECISION]), + ) + self.assertEqual( + 0, len(test_notification_center.notification_listeners[enums.NotificationTypes.TRACK]), + ) + self.assertEqual( + 0, len(test_notification_center.notification_listeners[enums.NotificationTypes.LOG_EVENT]), + ) # Remove one of the activate listeners and assert. self.assertTrue(test_notification_center.remove_notification_listener(3)) - self.assertEqual(1, len(test_notification_center.notification_listeners[enums.NotificationTypes.ACTIVATE])) + self.assertEqual( + 1, len(test_notification_center.notification_listeners[enums.NotificationTypes.ACTIVATE]), + ) def test_remove_notification_listener__invalid_listener(self): """ Test that removing a invalid notification listener returns False. """ @@ -164,19 +182,23 @@ def another_on_activate_listener(*args): # Add multiple notification listeners. self.assertEqual( 1, - test_notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, on_activate_listener) + test_notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, on_activate_listener), ) self.assertEqual( 2, - test_notification_center.add_notification_listener(enums.NotificationTypes.DECISION, on_decision_listener) + test_notification_center.add_notification_listener(enums.NotificationTypes.DECISION, on_decision_listener), ) self.assertEqual( - 3, test_notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, - another_on_activate_listener) + 3, + test_notification_center.add_notification_listener( + enums.NotificationTypes.ACTIVATE, another_on_activate_listener + ), ) self.assertEqual( - 4, test_notification_center.add_notification_listener(enums.NotificationTypes.LOG_EVENT, - on_log_event_listener) + 4, + test_notification_center.add_notification_listener( + enums.NotificationTypes.LOG_EVENT, on_log_event_listener + ), ) # Try removing a listener which does not exist. @@ -190,19 +212,24 @@ def test_clear_notification_listeners(self): # Add listeners test_notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, on_activate_listener) - test_notification_center.add_notification_listener(enums.NotificationTypes.OPTIMIZELY_CONFIG_UPDATE, - on_config_update_listener) + test_notification_center.add_notification_listener( + enums.NotificationTypes.OPTIMIZELY_CONFIG_UPDATE, on_config_update_listener + ) test_notification_center.add_notification_listener(enums.NotificationTypes.DECISION, on_decision_listener) test_notification_center.add_notification_listener(enums.NotificationTypes.TRACK, on_track_listener) test_notification_center.add_notification_listener(enums.NotificationTypes.LOG_EVENT, on_log_event_listener) # Assert all listeners are there: for notification_type in notification_center.NOTIFICATION_TYPES: - self.assertEqual(1, len(test_notification_center.notification_listeners[notification_type])) + self.assertEqual( + 1, len(test_notification_center.notification_listeners[notification_type]), + ) # Clear all of type DECISION. test_notification_center.clear_notification_listeners(enums.NotificationTypes.DECISION) - self.assertEqual(0, len(test_notification_center.notification_listeners[enums.NotificationTypes.DECISION])) + self.assertEqual( + 0, len(test_notification_center.notification_listeners[enums.NotificationTypes.DECISION]), + ) def test_clear_notification_listeners__invalid_type(self): """ Test that clear_notification_listener logs error if provided notification type is invalid. """ @@ -211,8 +238,9 @@ def test_clear_notification_listeners__invalid_type(self): test_notification_center = notification_center.NotificationCenter(logger=mock_logger) test_notification_center.clear_notification_listeners('invalid_notification_type') - mock_logger.error.assert_called_once_with('Invalid notification_type: invalid_notification_type provided. ' - 'Not removing any listener.') + mock_logger.error.assert_called_once_with( + 'Invalid notification_type: invalid_notification_type provided. ' 'Not removing any listener.' + ) def test_clear_all_notification_listeners(self): """ Test that all notification listeners are cleared on using the clear all API. """ @@ -221,21 +249,26 @@ def test_clear_all_notification_listeners(self): # Add listeners test_notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, on_activate_listener) - test_notification_center.add_notification_listener(enums.NotificationTypes.OPTIMIZELY_CONFIG_UPDATE, - on_config_update_listener) + test_notification_center.add_notification_listener( + enums.NotificationTypes.OPTIMIZELY_CONFIG_UPDATE, on_config_update_listener + ) test_notification_center.add_notification_listener(enums.NotificationTypes.DECISION, on_decision_listener) test_notification_center.add_notification_listener(enums.NotificationTypes.TRACK, on_track_listener) test_notification_center.add_notification_listener(enums.NotificationTypes.LOG_EVENT, on_log_event_listener) # Assert all listeners are there: for notification_type in notification_center.NOTIFICATION_TYPES: - self.assertEqual(1, len(test_notification_center.notification_listeners[notification_type])) + self.assertEqual( + 1, len(test_notification_center.notification_listeners[notification_type]), + ) # Clear all and assert again. test_notification_center.clear_all_notification_listeners() for notification_type in notification_center.NOTIFICATION_TYPES: - self.assertEqual(0, len(test_notification_center.notification_listeners[notification_type])) + self.assertEqual( + 0, len(test_notification_center.notification_listeners[notification_type]), + ) def set_listener_called_to_true(self): """ Helper method which sets the value of listener_called to True. Used to test sending of notifications.""" @@ -246,8 +279,9 @@ def test_send_notifications(self): test_notification_center = notification_center.NotificationCenter() self.listener_called = False - test_notification_center.add_notification_listener(enums.NotificationTypes.DECISION, - self.set_listener_called_to_true) + test_notification_center.add_notification_listener( + enums.NotificationTypes.DECISION, self.set_listener_called_to_true + ) test_notification_center.send_notifications(enums.NotificationTypes.DECISION) self.assertTrue(self.listener_called) @@ -257,8 +291,9 @@ def test_send_notifications__invalid_notification_type(self): mock_logger = mock.Mock() test_notification_center = notification_center.NotificationCenter(logger=mock_logger) test_notification_center.send_notifications('invalid_notification_type') - mock_logger.error.assert_called_once_with('Invalid notification_type: invalid_notification_type provided. ' - 'Not triggering any notification.') + mock_logger.error.assert_called_once_with( + 'Invalid notification_type: invalid_notification_type provided. ' 'Not triggering any notification.' + ) def test_send_notifications__fails(self): """ Test that send_notifications logs exception when call back fails. """ @@ -269,10 +304,10 @@ def some_listener(arg_1, arg_2): mock_logger = mock.Mock() test_notification_center = notification_center.NotificationCenter(logger=mock_logger) - test_notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, - some_listener) + test_notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, some_listener) # Not providing any of the 2 expected arguments during send. test_notification_center.send_notifications(enums.NotificationTypes.ACTIVATE) mock_logger.exception.assert_called_once_with( - 'Unknown problem when sending "{}" type notification.'.format(enums.NotificationTypes.ACTIVATE)) + 'Unknown problem when sending "{}" type notification.'.format(enums.NotificationTypes.ACTIVATE) + ) diff --git a/tests/test_optimizely.py b/tests/test_optimizely.py index d1e8dc0d..39978451 100644 --- a/tests/test_optimizely.py +++ b/tests/test_optimizely.py @@ -32,4067 +32,4309 @@ class OptimizelyTest(base.BaseTest): - strTest = None + strTest = None - try: - isinstance("test", basestring) # attempt to evaluate basestring + try: + isinstance("test", basestring) # attempt to evaluate basestring - _expected_notification_failure = 'Problem calling notify callback.' + _expected_notification_failure = 'Problem calling notify callback.' - def isstr(self, s): - return isinstance(s, basestring) + def isstr(self, s): + return isinstance(s, basestring) - strTest = isstr + strTest = isstr - except NameError: + except NameError: - def isstr(self, s): - return isinstance(s, str) - strTest = isstr + def isstr(self, s): + return isinstance(s, str) - def _validate_event_object(self, event_obj, expected_url, expected_params, expected_verb, expected_headers): - """ Helper method to validate properties of the event object. """ + strTest = isstr - self.assertEqual(expected_url, event_obj.get('url')) + def _validate_event_object(self, event_obj, expected_url, expected_params, expected_verb, expected_headers): + """ Helper method to validate properties of the event object. """ - event_params = event_obj.get('params') + self.assertEqual(expected_url, event_obj.get('url')) - expected_params['visitors'][0]['attributes'] = \ - sorted(expected_params['visitors'][0]['attributes'], key=itemgetter('key')) - event_params['visitors'][0]['attributes'] = \ - sorted(event_params['visitors'][0]['attributes'], key=itemgetter('key')) - self.assertEqual(expected_params, event_params) - self.assertEqual(expected_verb, event_obj.get('http_verb')) - self.assertEqual(expected_headers, event_obj.get('headers')) + event_params = event_obj.get('params') - def _validate_event_object_event_tags(self, event_obj, expected_event_metric_params, expected_event_features_params): - """ Helper method to validate properties of the event object related to event tags. """ + expected_params['visitors'][0]['attributes'] = sorted( + expected_params['visitors'][0]['attributes'], key=itemgetter('key') + ) + event_params['visitors'][0]['attributes'] = sorted( + event_params['visitors'][0]['attributes'], key=itemgetter('key') + ) + self.assertEqual(expected_params, event_params) + self.assertEqual(expected_verb, event_obj.get('http_verb')) + self.assertEqual(expected_headers, event_obj.get('headers')) - event_params = event_obj.get('params') + def _validate_event_object_event_tags( + self, event_obj, expected_event_metric_params, expected_event_features_params + ): + """ Helper method to validate properties of the event object related to event tags. """ - # get event metrics from the created event object - event_metrics = event_params['visitors'][0]['snapshots'][0]['events'][0]['tags'] - self.assertEqual(expected_event_metric_params, event_metrics) + event_params = event_obj.get('params') - # get event features from the created event object - event_features = event_params['visitors'][0]['attributes'][0] - self.assertEqual(expected_event_features_params, event_features) + # get event metrics from the created event object + event_metrics = event_params['visitors'][0]['snapshots'][0]['events'][0]['tags'] + self.assertEqual(expected_event_metric_params, event_metrics) - def test_init__invalid_datafile__logs_error(self): - """ Test that invalid datafile logs error on init. """ + # get event features from the created event object + event_features = event_params['visitors'][0]['attributes'][0] + self.assertEqual(expected_event_features_params, event_features) - mock_client_logger = mock.MagicMock() - with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger): - opt_obj = optimizely.Optimizely('invalid_datafile') + def test_init__invalid_datafile__logs_error(self): + """ Test that invalid datafile logs error on init. """ - mock_client_logger.error.assert_called_once_with('Provided "datafile" is in an invalid format.') - self.assertIsNone(opt_obj.config_manager.get_config()) + mock_client_logger = mock.MagicMock() + with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger): + opt_obj = optimizely.Optimizely('invalid_datafile') - def test_init__null_datafile__logs_error(self): - """ Test that null datafile logs error on init. """ + mock_client_logger.error.assert_called_once_with('Provided "datafile" is in an invalid format.') + self.assertIsNone(opt_obj.config_manager.get_config()) - mock_client_logger = mock.MagicMock() - with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger): - opt_obj = optimizely.Optimizely(None) + def test_init__null_datafile__logs_error(self): + """ Test that null datafile logs error on init. """ - mock_client_logger.error.assert_called_once_with('Provided "datafile" is in an invalid format.') - self.assertIsNone(opt_obj.config_manager.get_config()) + mock_client_logger = mock.MagicMock() + with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger): + opt_obj = optimizely.Optimizely(None) - def test_init__empty_datafile__logs_error(self): - """ Test that empty datafile logs error on init. """ + mock_client_logger.error.assert_called_once_with('Provided "datafile" is in an invalid format.') + self.assertIsNone(opt_obj.config_manager.get_config()) - mock_client_logger = mock.MagicMock() - with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger): - opt_obj = optimizely.Optimizely("") + def test_init__empty_datafile__logs_error(self): + """ Test that empty datafile logs error on init. """ - mock_client_logger.error.assert_called_once_with('Provided "datafile" is in an invalid format.') - self.assertIsNone(opt_obj.config_manager.get_config()) + mock_client_logger = mock.MagicMock() + with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger): + opt_obj = optimizely.Optimizely("") - def test_init__invalid_config_manager__logs_error(self): - """ Test that invalid config_manager logs error on init. """ + mock_client_logger.error.assert_called_once_with('Provided "datafile" is in an invalid format.') + self.assertIsNone(opt_obj.config_manager.get_config()) - class InvalidConfigManager(object): - pass + def test_init__invalid_config_manager__logs_error(self): + """ Test that invalid config_manager logs error on init. """ - mock_client_logger = mock.MagicMock() - with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger): - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), config_manager=InvalidConfigManager()) + class InvalidConfigManager(object): + pass - mock_client_logger.exception.assert_called_once_with('Provided "config_manager" is in an invalid format.') - self.assertFalse(opt_obj.is_valid) + mock_client_logger = mock.MagicMock() + with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), config_manager=InvalidConfigManager()) - def test_init__invalid_event_dispatcher__logs_error(self): - """ Test that invalid event_dispatcher logs error on init. """ + mock_client_logger.exception.assert_called_once_with('Provided "config_manager" is in an invalid format.') + self.assertFalse(opt_obj.is_valid) - class InvalidDispatcher(object): - pass + def test_init__invalid_event_dispatcher__logs_error(self): + """ Test that invalid event_dispatcher logs error on init. """ - mock_client_logger = mock.MagicMock() - with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger): - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), event_dispatcher=InvalidDispatcher) + class InvalidDispatcher(object): + pass - mock_client_logger.exception.assert_called_once_with('Provided "event_dispatcher" is in an invalid format.') - self.assertFalse(opt_obj.is_valid) + mock_client_logger = mock.MagicMock() + with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), event_dispatcher=InvalidDispatcher) - def test_init__invalid_event_processor__logs_error(self): - """ Test that invalid event_processor logs error on init. """ + mock_client_logger.exception.assert_called_once_with('Provided "event_dispatcher" is in an invalid format.') + self.assertFalse(opt_obj.is_valid) - class InvalidProcessor(object): - pass + def test_init__invalid_event_processor__logs_error(self): + """ Test that invalid event_processor logs error on init. """ - mock_client_logger = mock.MagicMock() - with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger): - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), event_processor=InvalidProcessor) + class InvalidProcessor(object): + pass - mock_client_logger.exception.assert_called_once_with('Provided "event_processor" is in an invalid format.') - self.assertFalse(opt_obj.is_valid) + mock_client_logger = mock.MagicMock() + with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), event_processor=InvalidProcessor) - def test_init__invalid_logger__logs_error(self): - """ Test that invalid logger logs error on init. """ + mock_client_logger.exception.assert_called_once_with('Provided "event_processor" is in an invalid format.') + self.assertFalse(opt_obj.is_valid) - class InvalidLogger(object): - pass + def test_init__invalid_logger__logs_error(self): + """ Test that invalid logger logs error on init. """ - mock_client_logger = mock.MagicMock() - with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger): - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), logger=InvalidLogger) + class InvalidLogger(object): + pass - mock_client_logger.exception.assert_called_once_with('Provided "logger" is in an invalid format.') - self.assertFalse(opt_obj.is_valid) + mock_client_logger = mock.MagicMock() + with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), logger=InvalidLogger) - def test_init__invalid_error_handler__logs_error(self): - """ Test that invalid error_handler logs error on init. """ + mock_client_logger.exception.assert_called_once_with('Provided "logger" is in an invalid format.') + self.assertFalse(opt_obj.is_valid) - class InvalidErrorHandler(object): - pass + def test_init__invalid_error_handler__logs_error(self): + """ Test that invalid error_handler logs error on init. """ - mock_client_logger = mock.MagicMock() - with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger): - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), error_handler=InvalidErrorHandler) + class InvalidErrorHandler(object): + pass - mock_client_logger.exception.assert_called_once_with('Provided "error_handler" is in an invalid format.') - self.assertFalse(opt_obj.is_valid) + mock_client_logger = mock.MagicMock() + with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), error_handler=InvalidErrorHandler) - def test_init__invalid_notification_center__logs_error(self): - """ Test that invalid notification_center logs error on init. """ + mock_client_logger.exception.assert_called_once_with('Provided "error_handler" is in an invalid format.') + self.assertFalse(opt_obj.is_valid) - class InvalidNotificationCenter(object): - pass + def test_init__invalid_notification_center__logs_error(self): + """ Test that invalid notification_center logs error on init. """ - mock_client_logger = mock.MagicMock() - with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger): - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), notification_center=InvalidNotificationCenter()) + class InvalidNotificationCenter(object): + pass - mock_client_logger.exception.assert_called_once_with('Provided "notification_center" is in an invalid format.') - self.assertFalse(opt_obj.is_valid) + mock_client_logger = mock.MagicMock() + with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger): + opt_obj = optimizely.Optimizely( + json.dumps(self.config_dict), notification_center=InvalidNotificationCenter(), + ) - def test_init__unsupported_datafile_version__logs_error(self): - """ Test that datafile with unsupported version logs error on init. """ + mock_client_logger.exception.assert_called_once_with('Provided "notification_center" is in an invalid format.') + self.assertFalse(opt_obj.is_valid) - 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') as mock_error_handler: - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_unsupported_version)) + def test_init__unsupported_datafile_version__logs_error(self): + """ Test that datafile with unsupported version logs error on init. """ - mock_client_logger.error.assert_called_once_with( - 'This version of the Python SDK does not support the given datafile version: "5".' - ) + 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' + ) as mock_error_handler: + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_unsupported_version)) - args, kwargs = mock_error_handler.call_args - self.assertIsInstance(args[0], exceptions.UnsupportedDatafileVersionException) - self.assertEqual(args[0].args[0], - 'This version of the Python SDK does not support the given datafile version: "5".') - self.assertIsNone(opt_obj.config_manager.get_config()) + mock_client_logger.error.assert_called_once_with( + 'This version of the Python SDK does not support the given datafile version: "5".' + ) - def test_init_with_supported_datafile_version(self): - """ Test that datafile with supported version works as expected. """ - - self.assertTrue(self.config_dict['version'] in project_config.SUPPORTED_VERSIONS) - - mock_client_logger = mock.MagicMock() - with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger): - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict)) - - mock_client_logger.exception.assert_not_called() - self.assertTrue(opt_obj.is_valid) - - def test_init__datafile_only(self): - """ Test that if only datafile is provided then StaticConfigManager is used. """ - - opt_obj = optimizely.Optimizely(datafile=json.dumps(self.config_dict)) - self.assertIs(type(opt_obj.config_manager), config_manager.StaticConfigManager) - - 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'): - opt_obj = optimizely.Optimizely(sdk_key='test_sdk_key') - - self.assertIs(type(opt_obj.config_manager), config_manager.PollingConfigManager) - - 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'): - opt_obj = optimizely.Optimizely(datafile=json.dumps(self.config_dict), sdk_key='test_sdk_key') - - self.assertIs(type(opt_obj.config_manager), config_manager.PollingConfigManager) - - def test_invalid_json_raises_schema_validation_off(self): - """ Test that invalid JSON logs error if schema validation is turned off. """ - - # 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') as mock_error_handler: - opt_obj = optimizely.Optimizely('invalid_json', skip_json_validation=True) - - mock_client_logger.error.assert_called_once_with('Provided "datafile" is in an invalid format.') - args, kwargs = mock_error_handler.call_args - self.assertIsInstance(args[0], exceptions.InvalidInputException) - self.assertEqual(args[0].args[0], - 'Provided "datafile" is in an invalid format.') - self.assertIsNone(opt_obj.config_manager.get_config()) - - mock_client_logger.reset_mock() - mock_error_handler.reset_mock() - - # 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') as mock_error_handler: - opt_obj = optimizely.Optimizely({'version': '2', 'events': 'invalid_value', 'experiments': 'invalid_value'}, - skip_json_validation=True) - - mock_client_logger.error.assert_called_once_with('Provided "datafile" is in an invalid format.') - args, kwargs = mock_error_handler.call_args - self.assertIsInstance(args[0], exceptions.InvalidInputException) - self.assertEqual(args[0].args[0], - 'Provided "datafile" is in an invalid format.') - self.assertIsNone(opt_obj.config_manager.get_config()) - - 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')) as mock_decision, \ - 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: - self.assertEqual('variation', self.optimizely.activate('test_experiment', 'test_user')) - - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [], - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111129', - 'experiment_id': '111127', - 'campaign_id': '111182' - }], - 'events': [{ - 'timestamp': 42000, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated', - }] - }] - }], - 'client_version': version.__version__, - 'client_name': 'python-sdk', - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) - - mock_decision.assert_called_once_with( - self.project_config, self.project_config.get_experiment_from_key('test_experiment'), 'test_user', None - ) - self.assertEqual(1, mock_process.call_count) - - self._validate_event_object(log_event.__dict__, - 'https://logx.optimizely.com/v1/events', - expected_params, 'POST', {'Content-Type': 'application/json'}) - - def test_add_activate_remove_clear_listener(self): - callbackhit = [False] - """ Test adding a listener activate passes correctly and gets called""" - def on_activate(experiment, user_id, attributes, variation, event): - self.assertTrue(isinstance(experiment, entities.Experiment)) - self.assertTrue(self.strTest(user_id)) - if attributes is not None: - self.assertTrue(isinstance(attributes, dict)) - self.assertTrue(isinstance(variation, entities.Variation)) - # self.assertTrue(isinstance(event, event_builder.Event)) - print("Activated experiment {0}".format(experiment.key)) - callbackhit[0] = True - - notification_id = self.optimizely.notification_center.add_notification_listener( - 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')), \ - mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process'): - self.assertEqual('variation', self.optimizely.activate('test_experiment', 'test_user')) - - self.assertEqual(True, callbackhit[0]) - self.optimizely.notification_center.remove_notification_listener(notification_id) - self.assertEqual(0, - len(self.optimizely.notification_center.notification_listeners[enums.NotificationTypes.ACTIVATE])) - self.optimizely.notification_center.clear_all_notifications() - self.assertEqual(0, - len(self.optimizely.notification_center.notification_listeners[enums.NotificationTypes.ACTIVATE])) - - def test_add_track_remove_clear_listener(self): - """ Test adding a listener track passes correctly and gets called""" - callback_hit = [False] - - def on_track(event_key, user_id, attributes, event_tags, event): - self.assertTrue(self.strTest(event_key)) - self.assertTrue(self.strTest(user_id)) - if attributes is not None: - self.assertTrue(isinstance(attributes, dict)) - if event_tags is not None: - self.assertTrue(isinstance(event_tags, dict)) - - self.assertTrue(isinstance(event, dict)) - callback_hit[0] = True - - 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')), \ - mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process'): - self.optimizely.track('test_event', 'test_user') - - self.assertEqual(True, callback_hit[0]) - - self.assertEqual(1, len(self.optimizely.notification_center.notification_listeners[enums.NotificationTypes.TRACK])) - self.optimizely.notification_center.remove_notification_listener(note_id) - self.assertEqual(0, len(self.optimizely.notification_center.notification_listeners[enums.NotificationTypes.TRACK])) - self.optimizely.notification_center.clear_all_notifications() - self.assertEqual(0, len(self.optimizely.notification_center.notification_listeners[enums.NotificationTypes.TRACK])) - - def test_activate_and_decision_listener(self): - """ Test that activate calls broadcast activate and decision with proper parameters. """ - - def on_activate(event_key, user_id, attributes, event_tags, event): - pass - - self.optimizely.notification_center.add_notification_listener( - 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')), \ - mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast: - self.assertEqual('variation', self.optimizely.activate('test_experiment', 'test_user')) - - log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) - - self.assertEqual(mock_broadcast.call_count, 2) - - mock_broadcast.assert_has_calls([ - mock.call( - enums.NotificationTypes.DECISION, - 'ab-test', - 'test_user', - {}, - { - 'experiment_key': 'test_experiment', - 'variation_key': 'variation' - } - ), - mock.call( - enums.NotificationTypes.ACTIVATE, - self.project_config.get_experiment_from_key('test_experiment'), - 'test_user', None, - self.project_config.get_variation_from_id('test_experiment', '111129'), - log_event.__dict__ - ) - ]) - - def test_activate_and_decision_listener_with_attr(self): - """ Test that activate calls broadcast activate and decision with proper parameters. """ - - def on_activate(event_key, user_id, attributes, event_tags, event): - pass - - self.optimizely.notification_center.add_notification_listener( - 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')), \ - mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast: - self.assertEqual('variation', - self.optimizely.activate('test_experiment', 'test_user', {'test_attribute': 'test_value'})) - - log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) - - self.assertEqual(mock_broadcast.call_count, 2) - - mock_broadcast.assert_has_calls([ - mock.call( - enums.NotificationTypes.DECISION, - 'ab-test', - 'test_user', - {'test_attribute': 'test_value'}, - { - 'experiment_key': 'test_experiment', - 'variation_key': 'variation' + args, kwargs = mock_error_handler.call_args + self.assertIsInstance(args[0], exceptions.UnsupportedDatafileVersionException) + self.assertEqual( + args[0].args[0], 'This version of the Python SDK does not support the given datafile version: "5".', + ) + self.assertIsNone(opt_obj.config_manager.get_config()) + + def test_init_with_supported_datafile_version(self): + """ Test that datafile with supported version works as expected. """ + + self.assertTrue(self.config_dict['version'] in project_config.SUPPORTED_VERSIONS) + + mock_client_logger = mock.MagicMock() + with mock.patch('optimizely.logger.reset_logger', return_value=mock_client_logger): + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict)) + + mock_client_logger.exception.assert_not_called() + self.assertTrue(opt_obj.is_valid) + + def test_init__datafile_only(self): + """ Test that if only datafile is provided then StaticConfigManager is used. """ + + opt_obj = optimizely.Optimizely(datafile=json.dumps(self.config_dict)) + self.assertIs(type(opt_obj.config_manager), config_manager.StaticConfigManager) + + 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' + ): + opt_obj = optimizely.Optimizely(sdk_key='test_sdk_key') + + self.assertIs(type(opt_obj.config_manager), config_manager.PollingConfigManager) + + 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' + ): + opt_obj = optimizely.Optimizely(datafile=json.dumps(self.config_dict), sdk_key='test_sdk_key') + + self.assertIs(type(opt_obj.config_manager), config_manager.PollingConfigManager) + + def test_invalid_json_raises_schema_validation_off(self): + """ Test that invalid JSON logs error if schema validation is turned off. """ + + # 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' + ) as mock_error_handler: + opt_obj = optimizely.Optimizely('invalid_json', skip_json_validation=True) + + mock_client_logger.error.assert_called_once_with('Provided "datafile" is in an invalid format.') + args, kwargs = mock_error_handler.call_args + self.assertIsInstance(args[0], exceptions.InvalidInputException) + self.assertEqual(args[0].args[0], 'Provided "datafile" is in an invalid format.') + self.assertIsNone(opt_obj.config_manager.get_config()) + + mock_client_logger.reset_mock() + mock_error_handler.reset_mock() + + # 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' + ) as mock_error_handler: + opt_obj = optimizely.Optimizely( + {'version': '2', 'events': 'invalid_value', 'experiments': 'invalid_value'}, skip_json_validation=True, + ) + + mock_client_logger.error.assert_called_once_with('Provided "datafile" is in an invalid format.') + args, kwargs = mock_error_handler.call_args + self.assertIsInstance(args[0], exceptions.InvalidInputException) + self.assertEqual(args[0].args[0], 'Provided "datafile" is in an invalid format.') + self.assertIsNone(opt_obj.config_manager.get_config()) + + 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'), + ) as mock_decision, 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: + self.assertEqual('variation', self.optimizely.activate('test_experiment', 'test_user')) + + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [], + 'snapshots': [ + { + 'decisions': [ + {'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'} + ], + 'events': [ + { + 'timestamp': 42000, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_version': version.__version__, + 'client_name': 'python-sdk', + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', } - ), - mock.call( - enums.NotificationTypes.ACTIVATE, - self.project_config.get_experiment_from_key('test_experiment'), - 'test_user', {'test_attribute': 'test_value'}, - self.project_config.get_variation_from_id('test_experiment', '111129'), - log_event.__dict__ - ) - ]) - - def test_decision_listener__user_not_in_experiment(self): - """ Test that activate calls broadcast decision with variation_key 'None' \ + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + mock_decision.assert_called_once_with( + self.project_config, self.project_config.get_experiment_from_key('test_experiment'), 'test_user', None, + ) + self.assertEqual(1, mock_process.call_count) + + self._validate_event_object( + log_event.__dict__, + 'https://logx.optimizely.com/v1/events', + expected_params, + 'POST', + {'Content-Type': 'application/json'}, + ) + + def test_add_activate_remove_clear_listener(self): + callbackhit = [False] + """ Test adding a listener activate passes correctly and gets called""" + + def on_activate(experiment, user_id, attributes, variation, event): + self.assertTrue(isinstance(experiment, entities.Experiment)) + self.assertTrue(self.strTest(user_id)) + if attributes is not None: + self.assertTrue(isinstance(attributes, dict)) + self.assertTrue(isinstance(variation, entities.Variation)) + # self.assertTrue(isinstance(event, event_builder.Event)) + print("Activated experiment {0}".format(experiment.key)) + callbackhit[0] = True + + notification_id = self.optimizely.notification_center.add_notification_listener( + 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'), + ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process'): + self.assertEqual('variation', self.optimizely.activate('test_experiment', 'test_user')) + + self.assertEqual(True, callbackhit[0]) + self.optimizely.notification_center.remove_notification_listener(notification_id) + self.assertEqual( + 0, len(self.optimizely.notification_center.notification_listeners[enums.NotificationTypes.ACTIVATE]), + ) + self.optimizely.notification_center.clear_all_notifications() + self.assertEqual( + 0, len(self.optimizely.notification_center.notification_listeners[enums.NotificationTypes.ACTIVATE]), + ) + + def test_add_track_remove_clear_listener(self): + """ Test adding a listener track passes correctly and gets called""" + callback_hit = [False] + + def on_track(event_key, user_id, attributes, event_tags, event): + self.assertTrue(self.strTest(event_key)) + self.assertTrue(self.strTest(user_id)) + if attributes is not None: + self.assertTrue(isinstance(attributes, dict)) + if event_tags is not None: + self.assertTrue(isinstance(event_tags, dict)) + + self.assertTrue(isinstance(event, dict)) + callback_hit[0] = True + + 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'), + ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process'): + self.optimizely.track('test_event', 'test_user') + + self.assertEqual(True, callback_hit[0]) + + self.assertEqual( + 1, len(self.optimizely.notification_center.notification_listeners[enums.NotificationTypes.TRACK]), + ) + self.optimizely.notification_center.remove_notification_listener(note_id) + self.assertEqual( + 0, len(self.optimizely.notification_center.notification_listeners[enums.NotificationTypes.TRACK]), + ) + self.optimizely.notification_center.clear_all_notifications() + self.assertEqual( + 0, len(self.optimizely.notification_center.notification_listeners[enums.NotificationTypes.TRACK]), + ) + + def test_activate_and_decision_listener(self): + """ Test that activate calls broadcast activate and decision with proper parameters. """ + + def on_activate(event_key, user_id, attributes, event_tags, event): + pass + + self.optimizely.notification_center.add_notification_listener(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'), + ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast: + self.assertEqual('variation', self.optimizely.activate('test_experiment', 'test_user')) + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + self.assertEqual(mock_broadcast.call_count, 2) + + mock_broadcast.assert_has_calls( + [ + mock.call( + enums.NotificationTypes.DECISION, + 'ab-test', + 'test_user', + {}, + {'experiment_key': 'test_experiment', 'variation_key': 'variation'}, + ), + mock.call( + enums.NotificationTypes.ACTIVATE, + self.project_config.get_experiment_from_key('test_experiment'), + 'test_user', + None, + self.project_config.get_variation_from_id('test_experiment', '111129'), + log_event.__dict__, + ), + ] + ) + + def test_activate_and_decision_listener_with_attr(self): + """ Test that activate calls broadcast activate and decision with proper parameters. """ + + def on_activate(event_key, user_id, attributes, event_tags, event): + pass + + self.optimizely.notification_center.add_notification_listener(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'), + ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast: + self.assertEqual( + 'variation', self.optimizely.activate('test_experiment', 'test_user', {'test_attribute': 'test_value'}), + ) + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + self.assertEqual(mock_broadcast.call_count, 2) + + mock_broadcast.assert_has_calls( + [ + mock.call( + enums.NotificationTypes.DECISION, + 'ab-test', + 'test_user', + {'test_attribute': 'test_value'}, + {'experiment_key': 'test_experiment', 'variation_key': 'variation'}, + ), + mock.call( + enums.NotificationTypes.ACTIVATE, + self.project_config.get_experiment_from_key('test_experiment'), + 'test_user', + {'test_attribute': 'test_value'}, + self.project_config.get_variation_from_id('test_experiment', '111129'), + log_event.__dict__, + ), + ] + ) + + 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('optimizely.event.event_processor.ForwardingEventProcessor.process'), \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: - self.assertEqual(None, self.optimizely.activate('test_experiment', 'test_user')) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'ab-test', - 'test_user', - {}, - { - 'experiment_key': 'test_experiment', - 'variation_key': None - } - ) - - def test_track_listener(self): - """ Test that track calls notification broadcaster. """ - - def on_track(event_key, user_id, attributes, event_tags, event): - pass - - 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' - )), \ - mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_event_tracked: - self.optimizely.track('test_event', 'test_user') - - log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) - - mock_event_tracked.assert_called_once_with(enums.NotificationTypes.TRACK, "test_event", - 'test_user', None, None, log_event.__dict__) - - def test_track_listener_with_attr(self): - """ Test that track calls notification broadcaster. """ - - def on_track(event_key, user_id, attributes, event_tags, event): - pass - - 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' - )), \ - mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_event_tracked: - self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'test_value'}) - - log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) - - mock_event_tracked.assert_called_once_with(enums.NotificationTypes.TRACK, "test_event", 'test_user', - {'test_attribute': 'test_value'}, - None, log_event.__dict__) - - def test_track_listener_with_attr_with_event_tags(self): - """ Test that track calls notification broadcaster. """ - - def on_track(event_key, user_id, attributes, event_tags, event): - pass - - 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' - )), \ - mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_event_tracked: - self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'test_value'}, - event_tags={'value': 1.234, 'non-revenue': 'abc'}) - - log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) - - mock_event_tracked.assert_called_once_with(enums.NotificationTypes.TRACK, "test_event", 'test_user', - {'test_attribute': 'test_value'}, - {'value': 1.234, 'non-revenue': 'abc'}, - log_event.__dict__) - - def test_is_feature_enabled__callback_listener(self): - """ Test that the feature is enabled for the user if bucketed into variation of an experiment. + with mock.patch('optimizely.decision_service.DecisionService.get_variation', return_value=None,), mock.patch( + 'optimizely.event.event_processor.ForwardingEventProcessor.process' + ), mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision: + self.assertEqual(None, self.optimizely.activate('test_experiment', 'test_user')) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'ab-test', + 'test_user', + {}, + {'experiment_key': 'test_experiment', 'variation_key': None}, + ) + + def test_track_listener(self): + """ Test that track calls notification broadcaster. """ + + def on_track(event_key, user_id, attributes, event_tags, event): + pass + + 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'), + ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_event_tracked: + self.optimizely.track('test_event', 'test_user') + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + mock_event_tracked.assert_called_once_with( + enums.NotificationTypes.TRACK, "test_event", 'test_user', None, None, log_event.__dict__, + ) + + def test_track_listener_with_attr(self): + """ Test that track calls notification broadcaster. """ + + def on_track(event_key, user_id, attributes, event_tags, event): + pass + + 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'), + ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_event_tracked: + self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'test_value'}) + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + mock_event_tracked.assert_called_once_with( + enums.NotificationTypes.TRACK, + "test_event", + 'test_user', + {'test_attribute': 'test_value'}, + None, + log_event.__dict__, + ) + + def test_track_listener_with_attr_with_event_tags(self): + """ Test that track calls notification broadcaster. """ + + def on_track(event_key, user_id, attributes, event_tags, event): + pass + + 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'), + ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_event_tracked: + self.optimizely.track( + 'test_event', + 'test_user', + attributes={'test_attribute': 'test_value'}, + event_tags={'value': 1.234, 'non-revenue': 'abc'}, + ) + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + mock_event_tracked.assert_called_once_with( + enums.NotificationTypes.TRACK, + "test_event", + 'test_user', + {'test_attribute': 'test_value'}, + {'value': 1.234, 'non-revenue': 'abc'}, + log_event.__dict__, + ) + + def test_is_feature_enabled__callback_listener(self): + """ Test that the feature is enabled for the user if bucketed into variation of an experiment. 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') + 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') - access_callback = [False] + access_callback = [False] - def on_activate(experiment, user_id, attributes, variation, event): - access_callback[0] = True + def on_activate(experiment, user_id, attributes, variation, event): + access_callback[0] = True - opt_obj.notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, on_activate) + opt_obj.notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, on_activate) - mock_experiment = project_config.get_experiment_from_key('test_experiment') - mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + mock_experiment = project_config.get_experiment_from_key('test_experiment') + 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.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')) + with mock.patch( + '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'): + 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) - self.assertTrue(access_callback[0]) + mock_decision.assert_called_once_with(opt_obj.config_manager.get_config(), feature, 'test_user', None) + self.assertTrue(access_callback[0]) - def test_is_feature_enabled_rollout_callback_listener(self): - """ Test that the feature is enabled for the user if bucketed into variation of a rollout. + def test_is_feature_enabled_rollout_callback_listener(self): + """ Test that the feature is enabled for the user if bucketed into variation of a rollout. Also confirm that no 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') + 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') - access_callback = [False] + access_callback = [False] - def on_activate(experiment, user_id, attributes, variation, event): - access_callback[0] = True + def on_activate(experiment, user_id, attributes, variation, event): + access_callback[0] = True - opt_obj.notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, on_activate) + opt_obj.notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, on_activate) - mock_experiment = project_config.get_experiment_from_key('test_experiment') - 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 - )) 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_experiment = project_config.get_experiment_from_key('test_experiment') + 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), + ) 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) + mock_decision.assert_called_once_with(project_config, feature, 'test_user', None) - # Check that impression event is not sent - self.assertEqual(0, mock_process.call_count) - self.assertEqual(False, access_callback[0]) + # Check that impression event is not sent + self.assertEqual(0, mock_process.call_count) + self.assertEqual(False, access_callback[0]) - def test_activate__with_attributes__audience_match(self): - """ Test that activate calls process with right params and returns expected + def test_activate__with_attributes__audience_match(self): + """ Test that activate calls process with right params and returns expected variation when attributes are provided and audience conditions are met. """ - with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation', - 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('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: - self.assertEqual('variation', self.optimizely.activate('test_experiment', 'test_user', - {'test_attribute': 'test_value'})) - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 'test_value', - 'entity_id': '111094', - 'key': 'test_attribute' - }], - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111129', - 'experiment_id': '111127', - 'campaign_id': '111182' - }], - 'events': [{ - 'timestamp': 42000, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated', - }] - }] - }], - 'client_version': version.__version__, - 'client_name': 'python-sdk', - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) - - 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'}) - self.assertEqual(1, mock_process.call_count) - self._validate_event_object(log_event.__dict__, - 'https://logx.optimizely.com/v1/events', - expected_params, 'POST', {'Content-Type': 'application/json'}) - - def test_activate__with_attributes_of_different_types(self): - """ Test that activate calls process with right params and returns expected + with mock.patch( + 'optimizely.decision_service.DecisionService.get_variation', + 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( + 'optimizely.event.event_processor.ForwardingEventProcessor.process' + ) as mock_process: + self.assertEqual( + 'variation', self.optimizely.activate('test_experiment', 'test_user', {'test_attribute': 'test_value'}), + ) + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + {'type': 'custom', 'value': 'test_value', 'entity_id': '111094', 'key': 'test_attribute'} + ], + 'snapshots': [ + { + 'decisions': [ + {'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'} + ], + 'events': [ + { + 'timestamp': 42000, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_version': version.__version__, + 'client_name': 'python-sdk', + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + 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'}, + ) + self.assertEqual(1, mock_process.call_count) + self._validate_event_object( + log_event.__dict__, + 'https://logx.optimizely.com/v1/events', + expected_params, + 'POST', + {'Content-Type': 'application/json'}, + ) + + def test_activate__with_attributes_of_different_types(self): + """ Test that activate calls process with right params and returns expected variation when different types of attributes are provided and audience conditions are met. """ - with mock.patch( - 'optimizely.bucketer.Bucketer.bucket', - 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, - 'integer_key': 0, - 'double_key': 0.0 + with mock.patch( + 'optimizely.bucketer.Bucketer.bucket', + 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, + 'integer_key': 0, + 'double_key': 0.0, + } + + self.assertEqual( + 'variation', self.optimizely.activate('test_experiment', 'test_user', attributes), + ) + + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + {'type': 'custom', 'value': False, 'entity_id': '111196', 'key': 'boolean_key'}, + {'type': 'custom', 'value': 0.0, 'entity_id': '111198', 'key': 'double_key'}, + {'type': 'custom', 'value': 0, 'entity_id': '111197', 'key': 'integer_key'}, + {'type': 'custom', 'value': 'test_value_1', 'entity_id': '111094', 'key': 'test_attribute'}, + ], + 'snapshots': [ + { + 'decisions': [ + {'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'} + ], + 'events': [ + { + 'timestamp': 42000, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_version': version.__version__, + 'client_name': 'python-sdk', + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', } - self.assertEqual('variation', self.optimizely.activate('test_experiment', 'test_user', attributes)) - - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': False, - 'entity_id': '111196', - 'key': 'boolean_key' - }, { - 'type': 'custom', - 'value': 0.0, - 'entity_id': '111198', - 'key': 'double_key' - }, { - 'type': 'custom', - 'value': 0, - 'entity_id': '111197', - 'key': 'integer_key' - }, { - 'type': 'custom', - 'value': 'test_value_1', - 'entity_id': '111094', - 'key': 'test_attribute' - }], - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111129', - 'experiment_id': '111127', - 'campaign_id': '111182' - }], - 'events': [{ - 'timestamp': 42000, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated', - }] - }] - }], - 'client_version': version.__version__, - 'client_name': 'python-sdk', - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) - - mock_bucket.assert_called_once_with( - self.project_config, self.project_config.get_experiment_from_key('test_experiment'), 'test_user', 'test_user' - ) - self.assertEqual(1, mock_process.call_count) - self._validate_event_object(log_event.__dict__, - 'https://logx.optimizely.com/v1/events', - expected_params, 'POST', {'Content-Type': 'application/json'}) - - def test_activate__with_attributes__typed_audience_match(self): - """ Test that activate calls process with right params and returns expected + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + mock_bucket.assert_called_once_with( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + 'test_user', + 'test_user', + ) + self.assertEqual(1, mock_process.call_count) + self._validate_event_object( + log_event.__dict__, + 'https://logx.optimizely.com/v1/events', + expected_params, + 'POST', + {'Content-Type': 'application/json'}, + ) + + def test_activate__with_attributes__typed_audience_match(self): + """ Test that activate calls process with right params and returns expected variation when attributes are provided and typed audience conditions are met. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - - with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: - # Should be included via exact match string audience with id '3468206642' - self.assertEqual('A', opt_obj.activate('typed_audience_experiment', 'test_user', - {'house': 'Gryffindor'})) - expected_attr = { - 'type': 'custom', - 'value': 'Gryffindor', - 'entity_id': '594015', - 'key': 'house' - } - - self.assertTrue( - expected_attr in [x.__dict__ for x in mock_process.call_args[0][0].visitor_attributes] - ) - - mock_process.reset() - - with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: - # Should be included via exact match number audience with id '3468206646' - self.assertEqual('A', opt_obj.activate('typed_audience_experiment', 'test_user', - {'lasers': 45.5})) - expected_attr = { - 'type': 'custom', - 'value': 45.5, - 'entity_id': '594016', - 'key': 'lasers' - } - - self.assertTrue( - expected_attr in [x.__dict__ for x in mock_process.call_args[0][0].visitor_attributes] - ) - - def test_activate__with_attributes__typed_audience_mismatch(self): - """ Test that activate returns None when typed audience conditions do not match. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - - with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: - self.assertIsNone(opt_obj.activate('typed_audience_experiment', 'test_user', - {'house': 'Hufflepuff'})) - self.assertEqual(0, mock_process.call_count) - - def test_activate__with_attributes__complex_audience_match(self): - """ Test that activate calls process with right params and returns expected - variation when attributes are provided and complex audience conditions are met. """ + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) + + with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + # Should be included via exact match string audience with id '3468206642' + self.assertEqual( + 'A', opt_obj.activate('typed_audience_experiment', 'test_user', {'house': 'Gryffindor'}), + ) + expected_attr = { + 'type': 'custom', + 'value': 'Gryffindor', + 'entity_id': '594015', + 'key': 'house', + } - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) + self.assertTrue(expected_attr in [x.__dict__ for x in mock_process.call_args[0][0].visitor_attributes]) - with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: - # Should be included via substring match string audience with id '3988293898', and - # exact match number audience with id '3468206646' - user_attr = {'house': 'Welcome to Slytherin!', 'lasers': 45.5} - self.assertEqual('A', opt_obj.activate('audience_combinations_experiment', 'test_user', user_attr)) + mock_process.reset() + + with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + # Should be included via exact match number audience with id '3468206646' + self.assertEqual( + 'A', opt_obj.activate('typed_audience_experiment', 'test_user', {'lasers': 45.5}), + ) + expected_attr = { + 'type': 'custom', + 'value': 45.5, + 'entity_id': '594016', + 'key': 'lasers', + } + + self.assertTrue(expected_attr in [x.__dict__ for x in mock_process.call_args[0][0].visitor_attributes]) + + def test_activate__with_attributes__typed_audience_mismatch(self): + """ Test that activate returns None when typed audience conditions do not match. """ + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) + + with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + self.assertIsNone(opt_obj.activate('typed_audience_experiment', 'test_user', {'house': 'Hufflepuff'})) + self.assertEqual(0, mock_process.call_count) + + def test_activate__with_attributes__complex_audience_match(self): + """ Test that activate calls process with right params and returns expected + variation when attributes are provided and complex audience conditions are met. """ - expected_attr_1 = { - 'type': 'custom', - 'value': 'Welcome to Slytherin!', - 'entity_id': '594015', - 'key': 'house' - } + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) + + with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + # Should be included via substring match string audience with id '3988293898', and + # exact match number audience with id '3468206646' + user_attr = {'house': 'Welcome to Slytherin!', 'lasers': 45.5} + self.assertEqual( + 'A', opt_obj.activate('audience_combinations_experiment', 'test_user', user_attr), + ) + + expected_attr_1 = { + 'type': 'custom', + 'value': 'Welcome to Slytherin!', + 'entity_id': '594015', + 'key': 'house', + } - expected_attr_2 = { - 'type': 'custom', - 'value': 45.5, - 'entity_id': '594016', - 'key': 'lasers' - } + expected_attr_2 = { + 'type': 'custom', + 'value': 45.5, + 'entity_id': '594016', + 'key': 'lasers', + } - self.assertTrue( - expected_attr_1 in [x.__dict__ for x in mock_process.call_args[0][0].visitor_attributes] - ) + self.assertTrue(expected_attr_1 in [x.__dict__ for x in mock_process.call_args[0][0].visitor_attributes]) - self.assertTrue( - expected_attr_2 in [x.__dict__ for x in mock_process.call_args[0][0].visitor_attributes] - ) + self.assertTrue(expected_attr_2 in [x.__dict__ for x in mock_process.call_args[0][0].visitor_attributes]) - def test_activate__with_attributes__complex_audience_mismatch(self): - """ Test that activate returns None when complex audience conditions do not match. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) + def test_activate__with_attributes__complex_audience_mismatch(self): + """ Test that activate returns None when complex audience conditions do not match. """ + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + 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)) + user_attr = {'house': 'Hufflepuff', 'lasers': 45.5} + self.assertIsNone(opt_obj.activate('audience_combinations_experiment', 'test_user', user_attr)) - self.assertEqual(0, mock_process.call_count) + self.assertEqual(0, mock_process.call_count) - def test_activate__with_attributes__audience_match__forced_bucketing(self): - """ Test that activate calls process with right params and returns expected + def test_activate__with_attributes__audience_match__forced_bucketing(self): + """ Test that activate calls process with right params and returns expected variation when attributes are provided and audience conditions are met after a set_forced_variation is called. """ - with 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: - self.assertTrue(self.optimizely.set_forced_variation('test_experiment', 'test_user', 'control')) - self.assertEqual('control', self.optimizely.activate('test_experiment', 'test_user', - {'test_attribute': 'test_value'})) - - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 'test_value', - 'entity_id': '111094', - 'key': 'test_attribute' - }], - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111128', - 'experiment_id': '111127', - 'campaign_id': '111182' - }], - 'events': [{ - 'timestamp': 42000, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated', - }] - }] - }], - 'client_version': version.__version__, - 'client_name': 'python-sdk', - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) - - self.assertEqual(1, mock_process.call_count) - self._validate_event_object(log_event.__dict__, - 'https://logx.optimizely.com/v1/events', - expected_params, 'POST', {'Content-Type': 'application/json'}) - - def test_activate__with_attributes__audience_match__bucketing_id_provided(self): - """ Test that activate calls process with right params and returns expected variation + with 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: + self.assertTrue(self.optimizely.set_forced_variation('test_experiment', 'test_user', 'control')) + self.assertEqual( + 'control', self.optimizely.activate('test_experiment', 'test_user', {'test_attribute': 'test_value'}), + ) + + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + {'type': 'custom', 'value': 'test_value', 'entity_id': '111094', 'key': 'test_attribute'} + ], + 'snapshots': [ + { + 'decisions': [ + {'variation_id': '111128', 'experiment_id': '111127', 'campaign_id': '111182'} + ], + 'events': [ + { + 'timestamp': 42000, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_version': version.__version__, + 'client_name': 'python-sdk', + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + self.assertEqual(1, mock_process.call_count) + self._validate_event_object( + log_event.__dict__, + 'https://logx.optimizely.com/v1/events', + expected_params, + 'POST', + {'Content-Type': 'application/json'}, + ) + + def test_activate__with_attributes__audience_match__bucketing_id_provided(self): + """ Test that activate calls process with right params and returns expected variation when attributes (including bucketing ID) are provided and audience conditions are met. """ - with mock.patch( + with mock.patch( 'optimizely.decision_service.DecisionService.get_variation', - 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('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: - self.assertEqual('variation', self.optimizely.activate('test_experiment', 'test_user', - {'test_attribute': 'test_value', - '$opt_bucketing_id': 'user_bucket_value'})) - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 'user_bucket_value', - 'entity_id': '$opt_bucketing_id', - 'key': '$opt_bucketing_id' - }, { - 'type': 'custom', - 'value': 'test_value', - 'entity_id': '111094', - 'key': 'test_attribute' - }], - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111129', - 'experiment_id': '111127', - 'campaign_id': '111182' - }], - 'events': [{ - 'timestamp': 42000, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated', - }] - }] - }], - 'client_version': version.__version__, - 'client_name': 'python-sdk', - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) - - 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'}) - self.assertEqual(1, mock_process.call_count) - self._validate_event_object(log_event.__dict__, - 'https://logx.optimizely.com/v1/events', - expected_params, 'POST', {'Content-Type': 'application/json'}) - - def test_activate__with_attributes__no_audience_match(self): - """ Test that activate returns None when audience conditions do not match. """ - - with mock.patch('optimizely.helpers.audience.is_user_in_experiment', return_value=False) as mock_audience_check: - self.assertIsNone(self.optimizely.activate('test_experiment', 'test_user', - attributes={'test_attribute': 'test_value'})) - mock_audience_check.assert_called_once_with(self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), - {'test_attribute': 'test_value'}, - self.optimizely.logger) - - 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') as mock_process: - self.assertIsNone(self.optimizely.activate('test_experiment', 'test_user', attributes='invalid')) - - self.assertEqual(0, mock_bucket.call_count) - self.assertEqual(0, mock_process.call_count) - - 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.is_user_in_experiment', 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('optimizely.bucketer.Bucketer.bucket') 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'})) - - mock_is_experiment_running.assert_called_once_with(self.project_config.get_experiment_from_key('test_experiment')) - self.assertEqual(0, mock_audience_check.call_count) - self.assertEqual(0, mock_bucket.call_count) - self.assertEqual(0, mock_process.call_count) - - 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.is_user_in_experiment', return_value=False) as mock_audience_check, \ - mock.patch('optimizely.helpers.experiment.is_experiment_running', - return_value=True) as mock_is_experiment_running: - self.assertEqual('control', self.optimizely.activate('test_experiment', 'user_1')) - mock_is_experiment_running.assert_called_once_with(self.project_config.get_experiment_from_key('test_experiment')) - self.assertEqual(0, mock_audience_check.call_count) - - def test_activate__bucketer_returns_none(self): - """ Test that activate returns None and does not process event when user is in no variation. """ - - with mock.patch('optimizely.helpers.audience.is_user_in_experiment', 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'})) - mock_bucket.assert_called_once_with(self.project_config, - self.project_config.get_experiment_from_key('test_experiment'), - 'test_user', - 'test_user') - self.assertEqual(0, mock_process.call_count) - - def test_activate__invalid_object(self): - """ Test that activate logs error if Optimizely instance is invalid. """ - - class InvalidConfigManager(object): - pass - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), config_manager=InvalidConfigManager()) - - with mock.patch.object(opt_obj, 'logger') as mock_client_logging: - self.assertIsNone(opt_obj.activate('test_experiment', 'test_user')) - - mock_client_logging.error.assert_called_once_with('Optimizely instance is not valid. Failing "activate".') - - def test_activate__invalid_config(self): - """ Test that activate logs error if config is invalid. """ - - opt_obj = optimizely.Optimizely('invalid_datafile') - - with mock.patch.object(opt_obj, 'logger') as mock_client_logging: - self.assertIsNone(opt_obj.activate('test_experiment', 'test_user')) - - mock_client_logging.error.assert_called_once_with('Invalid config. Optimizely instance is not valid. ' - 'Failing "activate".') - - 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'), \ - mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: - self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'test_value'}) - - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 'test_value', - 'entity_id': '111094', - 'key': 'test_attribute' - }], - 'snapshots': [{ - 'events': [{ - 'timestamp': 42000, - 'entity_id': '111095', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'test_event', - }] - }] - }], - 'client_version': version.__version__, - 'client_name': 'python-sdk', - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) - - self.assertEqual(1, mock_process.call_count) - self._validate_event_object(log_event.__dict__, - 'https://logx.optimizely.com/v1/events', - expected_params, 'POST', {'Content-Type': 'application/json'}) - - def test_track__with_attributes__typed_audience_match(self): - """ Test that track calls process with right params when attributes are provided + 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( + 'optimizely.event.event_processor.ForwardingEventProcessor.process' + ) as mock_process: + self.assertEqual( + 'variation', + self.optimizely.activate( + 'test_experiment', + 'test_user', + {'test_attribute': 'test_value', '$opt_bucketing_id': 'user_bucket_value'}, + ), + ) + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + { + 'type': 'custom', + 'value': 'user_bucket_value', + 'entity_id': '$opt_bucketing_id', + 'key': '$opt_bucketing_id', + }, + {'type': 'custom', 'value': 'test_value', 'entity_id': '111094', 'key': 'test_attribute'}, + ], + 'snapshots': [ + { + 'decisions': [ + {'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'} + ], + 'events': [ + { + 'timestamp': 42000, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_version': version.__version__, + 'client_name': 'python-sdk', + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + 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'}, + ) + self.assertEqual(1, mock_process.call_count) + self._validate_event_object( + log_event.__dict__, + 'https://logx.optimizely.com/v1/events', + expected_params, + 'POST', + {'Content-Type': 'application/json'}, + ) + + def test_activate__with_attributes__no_audience_match(self): + """ Test that activate returns None when audience conditions do not match. """ + + with mock.patch('optimizely.helpers.audience.is_user_in_experiment', return_value=False) as mock_audience_check: + self.assertIsNone( + self.optimizely.activate('test_experiment', 'test_user', attributes={'test_attribute': 'test_value'},) + ) + mock_audience_check.assert_called_once_with( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + {'test_attribute': 'test_value'}, + self.optimizely.logger, + ) + + 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' + ) as mock_process: + self.assertIsNone(self.optimizely.activate('test_experiment', 'test_user', attributes='invalid')) + + self.assertEqual(0, mock_bucket.call_count) + self.assertEqual(0, mock_process.call_count) + + 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.is_user_in_experiment', 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( + 'optimizely.bucketer.Bucketer.bucket' + ) 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'},) + ) + + mock_is_experiment_running.assert_called_once_with( + self.project_config.get_experiment_from_key('test_experiment') + ) + self.assertEqual(0, mock_audience_check.call_count) + self.assertEqual(0, mock_bucket.call_count) + self.assertEqual(0, mock_process.call_count) + + 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.is_user_in_experiment', return_value=False + ) as mock_audience_check, mock.patch( + 'optimizely.helpers.experiment.is_experiment_running', return_value=True + ) as mock_is_experiment_running: + self.assertEqual('control', self.optimizely.activate('test_experiment', 'user_1')) + mock_is_experiment_running.assert_called_once_with( + self.project_config.get_experiment_from_key('test_experiment') + ) + self.assertEqual(0, mock_audience_check.call_count) + + def test_activate__bucketer_returns_none(self): + """ Test that activate returns None and does not process event when user is in no variation. """ + + with mock.patch('optimizely.helpers.audience.is_user_in_experiment', 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'},) + ) + mock_bucket.assert_called_once_with( + self.project_config, + self.project_config.get_experiment_from_key('test_experiment'), + 'test_user', + 'test_user', + ) + self.assertEqual(0, mock_process.call_count) + + def test_activate__invalid_object(self): + """ Test that activate logs error if Optimizely instance is invalid. """ + + class InvalidConfigManager(object): + pass + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), config_manager=InvalidConfigManager()) + + with mock.patch.object(opt_obj, 'logger') as mock_client_logging: + self.assertIsNone(opt_obj.activate('test_experiment', 'test_user')) + + mock_client_logging.error.assert_called_once_with('Optimizely instance is not valid. Failing "activate".') + + def test_activate__invalid_config(self): + """ Test that activate logs error if config is invalid. """ + + opt_obj = optimizely.Optimizely('invalid_datafile') + + with mock.patch.object(opt_obj, 'logger') as mock_client_logging: + self.assertIsNone(opt_obj.activate('test_experiment', 'test_user')) + + mock_client_logging.error.assert_called_once_with( + 'Invalid config. Optimizely instance is not valid. ' 'Failing "activate".' + ) + + 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' + ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'test_value'}) + + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + {'type': 'custom', 'value': 'test_value', 'entity_id': '111094', 'key': 'test_attribute'} + ], + 'snapshots': [ + { + 'events': [ + { + 'timestamp': 42000, + 'entity_id': '111095', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'test_event', + } + ] + } + ], + } + ], + 'client_version': version.__version__, + 'client_name': 'python-sdk', + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + self.assertEqual(1, mock_process.call_count) + self._validate_event_object( + log_event.__dict__, + 'https://logx.optimizely.com/v1/events', + expected_params, + 'POST', + {'Content-Type': 'application/json'}, + ) + + def test_track__with_attributes__typed_audience_match(self): + """ Test that track calls process with right params when attributes are provided and it's a typed audience match. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: - # Should be included via substring match string audience with id '3988293898' - opt_obj.track('item_bought', 'test_user', {'house': 'Welcome to Slytherin!'}) + with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + # Should be included via substring match string audience with id '3988293898' + opt_obj.track('item_bought', 'test_user', {'house': 'Welcome to Slytherin!'}) - self.assertEqual(1, mock_process.call_count) + self.assertEqual(1, mock_process.call_count) - expected_attr = { - 'type': 'custom', - 'value': 'Welcome to Slytherin!', - 'entity_id': '594015', - 'key': 'house' - } + expected_attr = { + 'type': 'custom', + 'value': 'Welcome to Slytherin!', + 'entity_id': '594015', + 'key': 'house', + } - self.assertTrue( - expected_attr in [x.__dict__ for x in mock_process.call_args[0][0].visitor_attributes] - ) + self.assertTrue(expected_attr in [x.__dict__ for x in mock_process.call_args[0][0].visitor_attributes]) - def test_track__with_attributes__typed_audience_mismatch(self): - """ Test that track calls process even if audience conditions do not match. """ + def test_track__with_attributes__typed_audience_mismatch(self): + """ Test that track calls process even if audience conditions do not match. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: - opt_obj.track('item_bought', 'test_user', {'house': 'Welcome to Hufflepuff!'}) + with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + opt_obj.track('item_bought', 'test_user', {'house': 'Welcome to Hufflepuff!'}) - self.assertEqual(1, mock_process.call_count) + self.assertEqual(1, mock_process.call_count) - def test_track__with_attributes__complex_audience_match(self): - """ Test that track calls process with right params when attributes are provided + def test_track__with_attributes__complex_audience_match(self): + """ Test that track calls process with right params when attributes are provided and it's a complex audience match. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: - # Should be included via exact match string audience with id '3468206642', and - # exact match boolean audience with id '3468206643' - user_attr = {'house': 'Gryffindor', 'should_do_it': True} - opt_obj.track('user_signed_up', 'test_user', user_attr) + with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + # Should be included via exact match string audience with id '3468206642', and + # exact match boolean audience with id '3468206643' + user_attr = {'house': 'Gryffindor', 'should_do_it': True} + opt_obj.track('user_signed_up', 'test_user', user_attr) - self.assertEqual(1, mock_process.call_count) + self.assertEqual(1, mock_process.call_count) - expected_attr_1 = { - 'type': 'custom', - 'value': 'Gryffindor', - 'entity_id': '594015', - 'key': 'house' - } + expected_attr_1 = { + 'type': 'custom', + 'value': 'Gryffindor', + 'entity_id': '594015', + 'key': 'house', + } - self.assertTrue( - expected_attr_1 in [x.__dict__ for x in mock_process.call_args[0][0].visitor_attributes] - ) + self.assertTrue(expected_attr_1 in [x.__dict__ for x in mock_process.call_args[0][0].visitor_attributes]) - expected_attr_2 = { - 'type': 'custom', - 'value': True, - 'entity_id': '594017', - 'key': 'should_do_it' - } + expected_attr_2 = { + 'type': 'custom', + 'value': True, + 'entity_id': '594017', + 'key': 'should_do_it', + } - self.assertTrue( - expected_attr_2 in [x.__dict__ for x in mock_process.call_args[0][0].visitor_attributes] - ) + self.assertTrue(expected_attr_2 in [x.__dict__ for x in mock_process.call_args[0][0].visitor_attributes]) - def test_track__with_attributes__complex_audience_mismatch(self): - """ Test that track calls process even when complex audience conditions do not match. """ + def test_track__with_attributes__complex_audience_mismatch(self): + """ Test that track calls process even when complex audience conditions do not match. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: - # Should be excluded - exact match boolean audience with id '3468206643' does not match, - # so the overall conditions fail - user_attr = {'house': 'Gryffindor', 'should_do_it': False} - opt_obj.track('user_signed_up', 'test_user', user_attr) + with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + # Should be excluded - exact match boolean audience with id '3468206643' does not match, + # so the overall conditions fail + user_attr = {'house': 'Gryffindor', 'should_do_it': False} + opt_obj.track('user_signed_up', 'test_user', user_attr) - self.assertEqual(1, mock_process.call_count) + self.assertEqual(1, mock_process.call_count) - def test_track__with_attributes__bucketing_id_provided(self): - """ Test that track calls process with right params when + def test_track__with_attributes__bucketing_id_provided(self): + """ Test that track calls process with right params when 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'), \ - mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: - self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'test_value', - '$opt_bucketing_id': 'user_bucket_value'}) - - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 'user_bucket_value', - 'entity_id': '$opt_bucketing_id', - 'key': '$opt_bucketing_id' - }, { - 'type': 'custom', - 'value': 'test_value', - 'entity_id': '111094', - 'key': 'test_attribute' - }], - 'snapshots': [{ - 'events': [{ - 'timestamp': 42000, - 'entity_id': '111095', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'test_event', - }] - }] - }], - 'client_version': version.__version__, - 'client_name': 'python-sdk', - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) - - self.assertEqual(1, mock_process.call_count) - self._validate_event_object(log_event.__dict__, - 'https://logx.optimizely.com/v1/events', - expected_params, 'POST', {'Content-Type': 'application/json'}) - - 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') as mock_process: - self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'wrong_test_value'}) - - self.assertEqual(1, mock_process.call_count) - - 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') as mock_process: - self.optimizely.track('test_event', 'test_user', attributes='invalid') - - self.assertEqual(0, mock_bucket.call_count) - self.assertEqual(0, mock_process.call_count) - - 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'), \ - mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: - self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'test_value'}, - event_tags={'revenue': 4200, 'value': 1.234, 'non-revenue': 'abc'}) - - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 'test_value', - 'entity_id': '111094', - 'key': 'test_attribute' - }], - 'snapshots': [{ - 'events': [{ - 'entity_id': '111095', - 'key': 'test_event', - 'revenue': 4200, - 'tags': { - 'non-revenue': 'abc', - 'revenue': 4200, - 'value': 1.234, - }, - 'timestamp': 42000, - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'value': 1.234, - }] - }], - }], - 'client_version': version.__version__, - 'client_name': 'python-sdk', - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) - - self.assertEqual(1, mock_process.call_count) - self._validate_event_object(log_event.__dict__, - 'https://logx.optimizely.com/v1/events', - expected_params, 'POST', {'Content-Type': 'application/json'}) - - def test_track__with_event_tags_revenue(self): - """ Test that track calls process with right params when only revenue + with 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: + self.optimizely.track( + 'test_event', + 'test_user', + attributes={'test_attribute': 'test_value', '$opt_bucketing_id': 'user_bucket_value'}, + ) + + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + { + 'type': 'custom', + 'value': 'user_bucket_value', + 'entity_id': '$opt_bucketing_id', + 'key': '$opt_bucketing_id', + }, + {'type': 'custom', 'value': 'test_value', 'entity_id': '111094', 'key': 'test_attribute'}, + ], + 'snapshots': [ + { + 'events': [ + { + 'timestamp': 42000, + 'entity_id': '111095', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'test_event', + } + ] + } + ], + } + ], + 'client_version': version.__version__, + 'client_name': 'python-sdk', + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + self.assertEqual(1, mock_process.call_count) + self._validate_event_object( + log_event.__dict__, + 'https://logx.optimizely.com/v1/events', + expected_params, + 'POST', + {'Content-Type': 'application/json'}, + ) + + 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' + ) as mock_process: + self.optimizely.track( + 'test_event', 'test_user', attributes={'test_attribute': 'wrong_test_value'}, + ) + + self.assertEqual(1, mock_process.call_count) + + 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' + ) as mock_process: + self.optimizely.track('test_event', 'test_user', attributes='invalid') + + self.assertEqual(0, mock_bucket.call_count) + self.assertEqual(0, mock_process.call_count) + + 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' + ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + self.optimizely.track( + 'test_event', + 'test_user', + attributes={'test_attribute': 'test_value'}, + event_tags={'revenue': 4200, 'value': 1.234, 'non-revenue': 'abc'}, + ) + + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + {'type': 'custom', 'value': 'test_value', 'entity_id': '111094', 'key': 'test_attribute'} + ], + 'snapshots': [ + { + 'events': [ + { + 'entity_id': '111095', + 'key': 'test_event', + 'revenue': 4200, + 'tags': {'non-revenue': 'abc', 'revenue': 4200, 'value': 1.234}, + 'timestamp': 42000, + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'value': 1.234, + } + ] + } + ], + } + ], + 'client_version': version.__version__, + 'client_name': 'python-sdk', + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + self.assertEqual(1, mock_process.call_count) + self._validate_event_object( + log_event.__dict__, + 'https://logx.optimizely.com/v1/events', + expected_params, + 'POST', + {'Content-Type': 'application/json'}, + ) + + def test_track__with_event_tags_revenue(self): + """ Test that track calls process with right params when only revenue event tags are provided only. """ - with 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: - self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'test_value'}, - event_tags={'revenue': 4200, 'non-revenue': 'abc'}) - - expected_params = { - 'visitors': [{ - 'attributes': [{ - 'entity_id': '111094', - 'type': 'custom', - 'value': 'test_value', - 'key': 'test_attribute' - }], - 'visitor_id': 'test_user', - 'snapshots': [{ - 'events': [{ - 'entity_id': '111095', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'tags': { - 'non-revenue': 'abc', - 'revenue': 4200 - }, - 'timestamp': 42000, - 'revenue': 4200, - 'key': 'test_event' - }] - }] - }], - 'client_name': 'python-sdk', - 'project_id': '111001', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'account_id': '12001', - 'anonymize_ip': False, - 'revision': '42' - } - - log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) - - self.assertEqual(1, mock_process.call_count) - self._validate_event_object(log_event.__dict__, - 'https://logx.optimizely.com/v1/events', - expected_params, 'POST', {'Content-Type': 'application/json'}) - - def test_track__with_event_tags_numeric_metric(self): - """ Test that track calls process with right params when only numeric metric - event tags are provided. """ + with 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: + self.optimizely.track( + 'test_event', + 'test_user', + attributes={'test_attribute': 'test_value'}, + event_tags={'revenue': 4200, 'non-revenue': 'abc'}, + ) + + expected_params = { + 'visitors': [ + { + 'attributes': [ + {'entity_id': '111094', 'type': 'custom', 'value': 'test_value', 'key': 'test_attribute'} + ], + 'visitor_id': 'test_user', + 'snapshots': [ + { + 'events': [ + { + 'entity_id': '111095', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'tags': {'non-revenue': 'abc', 'revenue': 4200}, + 'timestamp': 42000, + 'revenue': 4200, + 'key': 'test_event', + } + ] + } + ], + } + ], + 'client_name': 'python-sdk', + 'project_id': '111001', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'account_id': '12001', + 'anonymize_ip': False, + 'revision': '42', + } - with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: - self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'test_value'}, - event_tags={'value': 1.234, 'non-revenue': 'abc'}) + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) - expected_event_metrics_params = { - 'non-revenue': 'abc', - 'value': 1.234 - } + self.assertEqual(1, mock_process.call_count) + self._validate_event_object( + log_event.__dict__, + 'https://logx.optimizely.com/v1/events', + expected_params, + 'POST', + {'Content-Type': 'application/json'}, + ) - expected_event_features_params = { - 'entity_id': '111094', - 'type': 'custom', - 'value': 'test_value', - 'key': 'test_attribute' - } + def test_track__with_event_tags_numeric_metric(self): + """ Test that track calls process with right params when only numeric metric + event tags are provided. """ - self.assertEqual(1, mock_process.call_count) + with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + self.optimizely.track( + 'test_event', + 'test_user', + attributes={'test_attribute': 'test_value'}, + event_tags={'value': 1.234, 'non-revenue': 'abc'}, + ) + + expected_event_metrics_params = {'non-revenue': 'abc', 'value': 1.234} + + expected_event_features_params = { + 'entity_id': '111094', + 'type': 'custom', + 'value': 'test_value', + 'key': 'test_attribute', + } + + self.assertEqual(1, mock_process.call_count) - log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) - self._validate_event_object_event_tags(log_event.__dict__, - expected_event_metrics_params, - expected_event_features_params) + self._validate_event_object_event_tags( + log_event.__dict__, expected_event_metrics_params, expected_event_features_params, + ) - def test_track__with_event_tags__forced_bucketing(self): - """ Test that track calls process with right params when event_value information is provided + def test_track__with_event_tags__forced_bucketing(self): + """ Test that track calls process with right params when event_value information is provided after a forced bucket. """ - with 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: - self.assertTrue(self.optimizely.set_forced_variation('test_experiment', 'test_user', 'variation')) - self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'test_value'}, - event_tags={'revenue': 4200, 'value': 1.234, 'non-revenue': 'abc'}) - - expected_params = { - 'account_id': '12001', - 'project_id': '111001', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': 'test_value', - 'entity_id': '111094', - 'key': 'test_attribute' - }], - 'snapshots': [{ - 'events': [{ - 'entity_id': '111095', - 'key': 'test_event', - 'revenue': 4200, - 'tags': { - 'non-revenue': 'abc', - 'revenue': 4200, - 'value': 1.234 - }, - 'timestamp': 42000, - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'value': 1.234, - }] - }], - }], - 'client_version': version.__version__, - 'client_name': 'python-sdk', - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '42' - } - - log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) - - self.assertEqual(1, mock_process.call_count) - self._validate_event_object(log_event.__dict__, - 'https://logx.optimizely.com/v1/events', - expected_params, 'POST', {'Content-Type': 'application/json'}) - - 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'), \ - mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: - self.optimizely.track('test_event', 'test_user', attributes={'test_attribute': 'test_value'}, - event_tags={'revenue': '4200', 'value': True}) - - expected_params = { - 'visitors': [{ - 'attributes': [{ - 'entity_id': '111094', - 'type': 'custom', - 'value': 'test_value', - 'key': 'test_attribute' - }], - 'visitor_id': 'test_user', - 'snapshots': [{ - 'events': [{ - 'timestamp': 42000, - 'entity_id': '111095', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'test_event', - 'tags': { - 'value': True, - 'revenue': '4200' - } - }] - }] - }], - 'client_name': 'python-sdk', - 'project_id': '111001', - 'client_version': version.__version__, - 'enrich_decisions': True, - 'account_id': '12001', - 'anonymize_ip': False, - 'revision': '42' - } + with 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: + self.assertTrue(self.optimizely.set_forced_variation('test_experiment', 'test_user', 'variation')) + self.optimizely.track( + 'test_event', + 'test_user', + attributes={'test_attribute': 'test_value'}, + event_tags={'revenue': 4200, 'value': 1.234, 'non-revenue': 'abc'}, + ) + + expected_params = { + 'account_id': '12001', + 'project_id': '111001', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + {'type': 'custom', 'value': 'test_value', 'entity_id': '111094', 'key': 'test_attribute'} + ], + 'snapshots': [ + { + 'events': [ + { + 'entity_id': '111095', + 'key': 'test_event', + 'revenue': 4200, + 'tags': {'non-revenue': 'abc', 'revenue': 4200, 'value': 1.234}, + 'timestamp': 42000, + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'value': 1.234, + } + ] + } + ], + } + ], + 'client_version': version.__version__, + 'client_name': 'python-sdk', + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '42', + } - log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) - self.assertEqual(1, mock_process.call_count) - self._validate_event_object(log_event.__dict__, - 'https://logx.optimizely.com/v1/events', - expected_params, 'POST', {'Content-Type': 'application/json'}) + self.assertEqual(1, mock_process.call_count) + self._validate_event_object( + log_event.__dict__, + 'https://logx.optimizely.com/v1/events', + expected_params, + 'POST', + {'Content-Type': 'application/json'}, + ) - def test_track__experiment_not_running(self): - """ Test that track calls process even if experiment is not running. """ + 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' + ), mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: + self.optimizely.track( + 'test_event', + 'test_user', + attributes={'test_attribute': 'test_value'}, + event_tags={'revenue': '4200', 'value': True}, + ) + + expected_params = { + 'visitors': [ + { + 'attributes': [ + {'entity_id': '111094', 'type': 'custom', 'value': 'test_value', 'key': 'test_attribute'} + ], + 'visitor_id': 'test_user', + 'snapshots': [ + { + 'events': [ + { + 'timestamp': 42000, + 'entity_id': '111095', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'test_event', + 'tags': {'value': True, 'revenue': '4200'}, + } + ] + } + ], + } + ], + 'client_name': 'python-sdk', + 'project_id': '111001', + 'client_version': version.__version__, + 'enrich_decisions': True, + 'account_id': '12001', + 'anonymize_ip': False, + 'revision': '42', + } + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + self.assertEqual(1, mock_process.call_count) + self._validate_event_object( + log_event.__dict__, + 'https://logx.optimizely.com/v1/events', + expected_params, + 'POST', + {'Content-Type': 'application/json'}, + ) - with mock.patch('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: - self.optimizely.track('test_event', 'test_user') + def test_track__experiment_not_running(self): + """ Test that track calls process even if experiment is not running. """ - # Assert that experiment is running is not performed - self.assertEqual(0, mock_is_experiment_running.call_count) - self.assertEqual(1, mock_process.call_count) + with mock.patch( + '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: + self.optimizely.track('test_event', 'test_user') - def test_track_invalid_event_key(self): - """ Test that track does not call process when event does not exist. """ + # Assert that experiment is running is not performed + self.assertEqual(0, mock_is_experiment_running.call_count) + self.assertEqual(1, mock_process.call_count) - with mock.patch('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') + def test_track_invalid_event_key(self): + """ Test that track does not call process when event does not exist. """ - self.assertEqual(0, mock_process.call_count) - mock_client_logging.info.assert_called_with( - 'Not tracking user "test_user" for event "aabbcc_event".' - ) + with mock.patch( + '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') - def test_track__whitelisted_user_overrides_audience_check(self): - """ Test that event is tracked when user is whitelisted. """ + self.assertEqual(0, mock_process.call_count) + mock_client_logging.info.assert_called_with('Not tracking user "test_user" for event "aabbcc_event".') - with 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: - self.optimizely.track('test_event', 'user_1') + def test_track__whitelisted_user_overrides_audience_check(self): + """ Test that event is tracked when user is whitelisted. """ - self.assertEqual(1, mock_process.call_count) + with 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: + self.optimizely.track('test_event', 'user_1') - def test_track__invalid_object(self): - """ Test that track logs error if Optimizely instance is invalid. """ + self.assertEqual(1, mock_process.call_count) - class InvalidConfigManager(object): - pass + def test_track__invalid_object(self): + """ Test that track logs error if Optimizely instance is invalid. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), config_manager=InvalidConfigManager()) + class InvalidConfigManager(object): + pass - with mock.patch.object(opt_obj, 'logger') as mock_client_logging: - self.assertIsNone(opt_obj.track('test_event', 'test_user')) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), config_manager=InvalidConfigManager()) - mock_client_logging.error.assert_called_once_with('Optimizely instance is not valid. Failing "track".') + with mock.patch.object(opt_obj, 'logger') as mock_client_logging: + self.assertIsNone(opt_obj.track('test_event', 'test_user')) - def test_track__invalid_config(self): - """ Test that track logs error if config is invalid. """ + mock_client_logging.error.assert_called_once_with('Optimizely instance is not valid. Failing "track".') - opt_obj = optimizely.Optimizely('invalid_datafile') + def test_track__invalid_config(self): + """ Test that track logs error if config is invalid. """ - with mock.patch.object(opt_obj, 'logger') as mock_client_logging: - opt_obj.track('test_event', 'test_user') + opt_obj = optimizely.Optimizely('invalid_datafile') - mock_client_logging.error.assert_called_once_with('Invalid config. Optimizely instance is not valid. ' - 'Failing "track".') + with mock.patch.object(opt_obj, 'logger') as mock_client_logging: + opt_obj.track('test_event', 'test_user') - def test_track__invalid_experiment_key(self): - """ Test that None is returned and expected log messages are logged during track \ + mock_client_logging.error.assert_called_once_with( + 'Invalid config. Optimizely instance is not valid. ' 'Failing "track".' + ) + + def test_track__invalid_experiment_key(self): + """ Test that None is returned and expected log messages are logged during track \ 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) as mock_validator: - self.assertIsNone(self.optimizely.track(99, 'test_user')) + with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, mock.patch( + 'optimizely.helpers.validator.is_non_empty_string', return_value=False + ) as mock_validator: + self.assertIsNone(self.optimizely.track(99, 'test_user')) - mock_validator.assert_any_call(99) + mock_validator.assert_any_call(99) - mock_client_logging.error.assert_called_once_with('Provided "event_key" is in an invalid format.') + mock_client_logging.error.assert_called_once_with('Provided "event_key" is in an invalid format.') - def test_track__invalid_user_id(self): - """ Test that None is returned and expected log messages are logged during track \ + def test_track__invalid_user_id(self): + """ Test that None is returned and expected log messages are logged during track \ when user_id is in invalid format. """ - with mock.patch.object(self.optimizely, 'logger') as mock_client_logging: - self.assertIsNone(self.optimizely.track('test_event', 99)) - mock_client_logging.error.assert_called_once_with('Provided "user_id" is in an invalid format.') - - 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')), \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast: - self.assertEqual('variation', self.optimizely.get_variation('test_experiment', 'test_user')) - - self.assertEqual(mock_broadcast.call_count, 1) - - mock_broadcast.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'ab-test', - 'test_user', - {}, - { - 'experiment_key': 'test_experiment', - 'variation_key': 'variation' - } - ) - - def test_get_variation_with_experiment_in_feature(self): - """ Test that get_variation returns valid variation and broadcasts decision listener with type feature-test when - get_variation returns feature experiment variation.""" + with mock.patch.object(self.optimizely, 'logger') as mock_client_logging: + self.assertIsNone(self.optimizely.track('test_event', 99)) + mock_client_logging.error.assert_called_once_with('Provided "user_id" is in an invalid format.') - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - project_config = opt_obj.config_manager.get_config() + def test_get_variation(self): + """ Test that get_variation returns valid variation and broadcasts decision with proper parameters. """ - with mock.patch( + with mock.patch( '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')) - - self.assertEqual(mock_broadcast.call_count, 1) - - mock_broadcast.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-test', - 'test_user', - {}, - { - 'experiment_key': 'test_experiment', - 'variation_key': 'variation' - } - ) + return_value=self.project_config.get_variation_from_id('test_experiment', '111129'), + ), mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast: + self.assertEqual( + 'variation', self.optimizely.get_variation('test_experiment', 'test_user'), + ) + + self.assertEqual(mock_broadcast.call_count, 1) + + mock_broadcast.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'ab-test', + 'test_user', + {}, + {'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. """ + def test_get_variation_with_experiment_in_feature(self): + """ Test that get_variation returns valid variation and broadcasts decision listener with type feature-test when + get_variation returns feature experiment variation.""" - with mock.patch( - 'optimizely.decision_service.DecisionService.get_variation', return_value=None), \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast: - self.assertEqual(None, self.optimizely.get_variation('test_experiment', 'test_user', - attributes={'test_attribute': 'test_value'})) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + project_config = opt_obj.config_manager.get_config() - self.assertEqual(mock_broadcast.call_count, 1) + with mock.patch( + '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')) + + self.assertEqual(mock_broadcast.call_count, 1) + + mock_broadcast.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-test', + 'test_user', + {}, + {'experiment_key': 'test_experiment', 'variation_key': 'variation'}, + ) - mock_broadcast.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'ab-test', - 'test_user', - {'test_attribute': 'test_value'}, - { - 'experiment_key': 'test_experiment', - 'variation_key': None - } - ) + 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( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast: + self.assertEqual( + None, + self.optimizely.get_variation( + 'test_experiment', 'test_user', attributes={'test_attribute': 'test_value'}, + ), + ) + + self.assertEqual(mock_broadcast.call_count, 1) + + mock_broadcast.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'ab-test', + 'test_user', + {'test_attribute': 'test_value'}, + {'experiment_key': 'test_experiment', 'variation_key': None}, + ) - def test_get_variation__invalid_object(self): - """ Test that get_variation logs error if Optimizely instance is invalid. """ + def test_get_variation__invalid_object(self): + """ Test that get_variation logs error if Optimizely instance is invalid. """ - class InvalidConfigManager(object): - pass + class InvalidConfigManager(object): + pass - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), config_manager=InvalidConfigManager()) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), config_manager=InvalidConfigManager()) - with mock.patch.object(opt_obj, 'logger') as mock_client_logging: - self.assertIsNone(opt_obj.get_variation('test_experiment', 'test_user')) + with mock.patch.object(opt_obj, 'logger') as mock_client_logging: + self.assertIsNone(opt_obj.get_variation('test_experiment', 'test_user')) - mock_client_logging.error.assert_called_once_with('Optimizely instance is not valid. Failing "get_variation".') + mock_client_logging.error.assert_called_once_with('Optimizely instance is not valid. Failing "get_variation".') - def test_get_variation__invalid_config(self): - """ Test that get_variation logs error if config is invalid. """ + def test_get_variation__invalid_config(self): + """ Test that get_variation logs error if config is invalid. """ - opt_obj = optimizely.Optimizely('invalid_datafile') + opt_obj = optimizely.Optimizely('invalid_datafile') - with mock.patch.object(opt_obj, 'logger') as mock_client_logging: - self.assertIsNone(opt_obj.get_variation('test_experiment', 'test_user')) + with mock.patch.object(opt_obj, 'logger') as mock_client_logging: + self.assertIsNone(opt_obj.get_variation('test_experiment', 'test_user')) - mock_client_logging.error.assert_called_once_with('Invalid config. Optimizely instance is not valid. ' - 'Failing "get_variation".') + mock_client_logging.error.assert_called_once_with( + 'Invalid config. Optimizely instance is not valid. ' 'Failing "get_variation".' + ) - def test_get_variation_unknown_experiment_key(self): - """ Test that get_variation retuns None when invalid experiment key is given. """ - with mock.patch.object(self.optimizely, 'logger') as mock_client_logging: - self.optimizely.get_variation('aabbccdd', 'test_user', None) + def test_get_variation_unknown_experiment_key(self): + """ Test that get_variation retuns None when invalid experiment key is given. """ + with mock.patch.object(self.optimizely, 'logger') as mock_client_logging: + self.optimizely.get_variation('aabbccdd', 'test_user', None) - mock_client_logging.info.assert_called_with( - 'Experiment key "aabbccdd" is invalid. Not activating user "test_user".' - ) + mock_client_logging.info.assert_called_with( + 'Experiment key "aabbccdd" is invalid. Not activating user "test_user".' + ) - def test_is_feature_enabled__returns_false_for_invalid_feature_key(self): - """ Test that is_feature_enabled returns false if the provided feature key is invalid. """ + def test_is_feature_enabled__returns_false_for_invalid_feature_key(self): + """ Test that is_feature_enabled returns false if the provided feature key is invalid. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + 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) as mock_validator: - self.assertFalse(opt_obj.is_feature_enabled(None, 'test_user')) + with mock.patch.object(opt_obj, 'logger') as mock_client_logging, mock.patch( + 'optimizely.helpers.validator.is_non_empty_string', return_value=False + ) as mock_validator: + self.assertFalse(opt_obj.is_feature_enabled(None, 'test_user')) - mock_validator.assert_any_call(None) - mock_client_logging.error.assert_called_with('Provided "feature_key" is in an invalid format.') + mock_validator.assert_any_call(None) + mock_client_logging.error.assert_called_with('Provided "feature_key" is in an invalid format.') - def test_is_feature_enabled__returns_false_for_invalid_user_id(self): - """ Test that is_feature_enabled returns false if the provided user ID is invalid. """ + def test_is_feature_enabled__returns_false_for_invalid_user_id(self): + """ Test that is_feature_enabled returns false if the provided user ID is invalid. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - with mock.patch.object(opt_obj, 'logger') as mock_client_logging: - self.assertFalse(opt_obj.is_feature_enabled('feature_key', 1.2)) - mock_client_logging.error.assert_called_with('Provided "user_id" is in an invalid format.') + with mock.patch.object(opt_obj, 'logger') as mock_client_logging: + self.assertFalse(opt_obj.is_feature_enabled('feature_key', 1.2)) + mock_client_logging.error.assert_called_with('Provided "user_id" is in an invalid format.') - def test_is_feature_enabled__returns_false_for__invalid_attributes(self): - """ Test that is_feature_enabled returns false if attributes are in an invalid format. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + def test_is_feature_enabled__returns_false_for__invalid_attributes(self): + """ Test that is_feature_enabled returns false if attributes are in an invalid format. """ + 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) as mock_validator: - self.assertFalse(opt_obj.is_feature_enabled('feature_key', 'test_user', attributes='invalid')) + with mock.patch.object(opt_obj, 'logger') as mock_client_logging, mock.patch( + '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')) - mock_validator.assert_called_once_with('invalid') - mock_client_logging.error.assert_called_once_with('Provided attributes are in an invalid format.') + mock_validator.assert_called_once_with('invalid') + mock_client_logging.error.assert_called_once_with('Provided attributes are in an invalid format.') - def test_is_feature_enabled__in_rollout__typed_audience_match(self): - """ Test that is_feature_enabled returns True for feature rollout with typed audience match. """ + def test_is_feature_enabled__in_rollout__typed_audience_match(self): + """ Test that is_feature_enabled returns True for feature rollout with typed audience match. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - # Should be included via exists match audience with id '3988293899' - self.assertTrue(opt_obj.is_feature_enabled('feat', 'test_user', {'favorite_ice_cream': 'chocolate'})) + # Should be included via exists match audience with id '3988293899' + self.assertTrue(opt_obj.is_feature_enabled('feat', 'test_user', {'favorite_ice_cream': 'chocolate'})) - # Should be included via less-than match audience with id '3468206644' - self.assertTrue(opt_obj.is_feature_enabled('feat', 'test_user', {'lasers': -3})) + # Should be included via less-than match audience with id '3468206644' + self.assertTrue(opt_obj.is_feature_enabled('feat', 'test_user', {'lasers': -3})) - def test_is_feature_enabled__in_rollout__typed_audience_mismatch(self): - """ Test that is_feature_enabled returns False for feature rollout with typed audience mismatch. """ + def test_is_feature_enabled__in_rollout__typed_audience_mismatch(self): + """ Test that is_feature_enabled returns False for feature rollout with typed audience mismatch. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - self.assertIs( - opt_obj.is_feature_enabled('feat', 'test_user', {}), - False - ) + self.assertIs(opt_obj.is_feature_enabled('feat', 'test_user', {}), False) - def test_is_feature_enabled__in_rollout__complex_audience_match(self): - """ Test that is_feature_enabled returns True for feature rollout with complex audience match. """ + def test_is_feature_enabled__in_rollout__complex_audience_match(self): + """ Test that is_feature_enabled returns True for feature rollout with complex audience match. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - # Should be included via substring match string audience with id '3988293898', and - # exists audience with id '3988293899' - user_attr = {'house': '...Slytherinnn...sss.', 'favorite_ice_cream': 'matcha'} - self.assertStrictTrue(opt_obj.is_feature_enabled('feat2', 'test_user', user_attr)) + # Should be included via substring match string audience with id '3988293898', and + # exists audience with id '3988293899' + user_attr = {'house': '...Slytherinnn...sss.', 'favorite_ice_cream': 'matcha'} + self.assertStrictTrue(opt_obj.is_feature_enabled('feat2', 'test_user', user_attr)) - def test_is_feature_enabled__in_rollout__complex_audience_mismatch(self): - """ Test that is_feature_enabled returns False for feature rollout with complex audience mismatch. """ + def test_is_feature_enabled__in_rollout__complex_audience_mismatch(self): + """ Test that is_feature_enabled returns False for feature rollout with complex audience mismatch. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - # Should be excluded - substring match string audience with id '3988293898' does not match, - # and no audience in the other branch of the 'and' matches either - self.assertStrictFalse(opt_obj.is_feature_enabled('feat2', 'test_user', {'house': 'Lannister'})) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) + # Should be excluded - substring match string audience with id '3988293898' does not match, + # and no audience in the other branch of the 'and' matches either + self.assertStrictFalse(opt_obj.is_feature_enabled('feat2', 'test_user', {'house': 'Lannister'})) - def test_is_feature_enabled__returns_false_for_invalid_feature(self): - """ Test that the feature is not enabled for the user if the provided feature key is invalid. """ + def test_is_feature_enabled__returns_false_for_invalid_feature(self): + """ Test that the feature is not enabled for the user if the provided feature key is invalid. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature') as mock_decision, \ - mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process: - self.assertFalse(opt_obj.is_feature_enabled('invalid_feature', 'user1')) + with mock.patch( + 'optimizely.decision_service.DecisionService.get_variation_for_feature' + ) as mock_decision, mock.patch( + 'optimizely.event.event_processor.ForwardingEventProcessor.process' + ) as mock_process: + self.assertFalse(opt_obj.is_feature_enabled('invalid_feature', 'user1')) - self.assertFalse(mock_decision.called) + self.assertFalse(mock_decision.called) - # Check that no event is sent - self.assertEqual(0, mock_process.call_count) + # 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): - """ Test that the feature is enabled for the user if bucketed into variation of an experiment and + 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 """ - 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') - - mock_experiment = project_config.get_experiment_from_key('test_experiment') - mock_variation = project_config.get_variation_from_id('test_experiment', '111129') - - # Assert that featureEnabled property is True - 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 - )) as mock_decision, \ - mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision, \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ - mock.patch('time.time', return_value=42): - 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) - - mock_broadcast_decision.assert_called_with( - enums.NotificationTypes.DECISION, - 'feature', - 'test_user', - {}, - { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': True, - 'source': 'feature-test', - 'source_info': { - 'experiment_key': 'test_experiment', - 'variation_key': 'variation' + 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') + + mock_experiment = project_config.get_experiment_from_key('test_experiment') + mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + + # Assert that featureEnabled property is True + 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), + ) as mock_decision, mock.patch( + 'optimizely.event.event_processor.ForwardingEventProcessor.process' + ) as mock_process, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision, mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ), mock.patch( + 'time.time', return_value=42 + ): + 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) + + mock_broadcast_decision.assert_called_with( + enums.NotificationTypes.DECISION, + 'feature', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': True, + 'source': 'feature-test', + 'source_info': {'experiment_key': 'test_experiment', 'variation_key': 'variation'}, + }, + ) + expected_params = { + 'account_id': '12001', + 'project_id': '111111', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + { + 'type': 'custom', + 'value': True, + 'entity_id': '$opt_bot_filtering', + 'key': '$opt_bot_filtering', + } + ], + 'snapshots': [ + { + 'decisions': [ + {'variation_id': '111129', 'experiment_id': '111127', 'campaign_id': '111182'} + ], + 'events': [ + { + 'timestamp': 42000, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_version': version.__version__, + 'client_name': 'python-sdk', + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '1', } - } - ) - expected_params = { - 'account_id': '12001', - 'project_id': '111111', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': True, - 'entity_id': '$opt_bot_filtering', - 'key': '$opt_bot_filtering' - }], - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111129', - 'experiment_id': '111127', - 'campaign_id': '111182' - }], - 'events': [{ - 'timestamp': 42000, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated', - }] - }] - }], - 'client_version': version.__version__, - 'client_name': 'python-sdk', - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '1' - } - - log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) - - # Check that impression event is sent - self.assertEqual(1, mock_process.call_count) - self._validate_event_object(log_event.__dict__, - 'https://logx.optimizely.com/v1/events', - expected_params, 'POST', {'Content-Type': 'application/json'}) - - 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 + + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + # Check that impression event is sent + self.assertEqual(1, mock_process.call_count) + self._validate_event_object( + log_event.__dict__, + 'https://logx.optimizely.com/v1/events', + expected_params, + 'POST', + {'Content-Type': 'application/json'}, + ) + + 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 """ - 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') - - mock_experiment = project_config.get_experiment_from_key('test_experiment') - mock_variation = project_config.get_variation_from_id('test_experiment', '111128') - - # Assert that featureEnabled property is False - 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 - )) as mock_decision, \ - mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision, \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ - mock.patch('time.time', return_value=42): - 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) - - mock_broadcast_decision.assert_called_with( - enums.NotificationTypes.DECISION, - 'feature', - 'test_user', - {}, - { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': False, - 'source': 'feature-test', - 'source_info': { - 'experiment_key': 'test_experiment', - 'variation_key': 'control' + 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') + + mock_experiment = project_config.get_experiment_from_key('test_experiment') + mock_variation = project_config.get_variation_from_id('test_experiment', '111128') + + # Assert that featureEnabled property is False + 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), + ) as mock_decision, mock.patch( + 'optimizely.event.event_processor.ForwardingEventProcessor.process' + ) as mock_process, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision, mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ), mock.patch( + 'time.time', return_value=42 + ): + 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) + + mock_broadcast_decision.assert_called_with( + enums.NotificationTypes.DECISION, + 'feature', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': False, + 'source': 'feature-test', + 'source_info': {'experiment_key': 'test_experiment', 'variation_key': 'control'}, + }, + ) + # Check that impression event is sent + expected_params = { + 'account_id': '12001', + 'project_id': '111111', + 'visitors': [ + { + 'visitor_id': 'test_user', + 'attributes': [ + { + 'type': 'custom', + 'value': True, + 'entity_id': '$opt_bot_filtering', + 'key': '$opt_bot_filtering', + } + ], + 'snapshots': [ + { + 'decisions': [ + {'variation_id': '111128', 'experiment_id': '111127', 'campaign_id': '111182'} + ], + 'events': [ + { + 'timestamp': 42000, + 'entity_id': '111182', + 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', + 'key': 'campaign_activated', + } + ], + } + ], + } + ], + 'client_version': version.__version__, + 'client_name': 'python-sdk', + 'enrich_decisions': True, + 'anonymize_ip': False, + 'revision': '1', } - } - ) - # Check that impression event is sent - expected_params = { - 'account_id': '12001', - 'project_id': '111111', - 'visitors': [{ - 'visitor_id': 'test_user', - 'attributes': [{ - 'type': 'custom', - 'value': True, - 'entity_id': '$opt_bot_filtering', - 'key': '$opt_bot_filtering' - }], - 'snapshots': [{ - 'decisions': [{ - 'variation_id': '111128', - 'experiment_id': '111127', - 'campaign_id': '111182' - }], - 'events': [{ - 'timestamp': 42000, - 'entity_id': '111182', - 'uuid': 'a68cf1ad-0393-4e18-af87-efe8f01a7c9c', - 'key': 'campaign_activated', - }] - }] - }], - 'client_version': version.__version__, - 'client_name': 'python-sdk', - 'enrich_decisions': True, - 'anonymize_ip': False, - 'revision': '1' - } - log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) - - # Check that impression event is sent - self.assertEqual(1, mock_process.call_count) - self._validate_event_object(log_event.__dict__, - 'https://logx.optimizely.com/v1/events', - expected_params, 'POST', {'Content-Type': 'application/json'}) - - 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 + log_event = EventFactory.create_log_event(mock_process.call_args[0][0], self.optimizely.logger) + + # Check that impression event is sent + self.assertEqual(1, mock_process.call_count) + self._validate_event_object( + log_event.__dict__, + 'https://logx.optimizely.com/v1/events', + expected_params, + 'POST', + {'Content-Type': 'application/json'}, + ) + + 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 """ - 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') - - mock_experiment = project_config.get_experiment_from_key('test_experiment') - mock_variation = project_config.get_variation_from_id('test_experiment', '111129') - - # Assert that featureEnabled property is True - 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 - )) as mock_decision, \ - mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision, \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ - mock.patch('time.time', return_value=42): - 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) - - mock_broadcast_decision.assert_called_with( - enums.NotificationTypes.DECISION, - 'feature', - 'test_user', - {}, - { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': True, - 'source': 'rollout', - 'source_info': {} - } - ) - - # Check that impression event is not sent - self.assertEqual(0, mock_process.call_count) - - 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 + 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') + + mock_experiment = project_config.get_experiment_from_key('test_experiment') + mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + + # Assert that featureEnabled property is True + 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), + ) as mock_decision, mock.patch( + 'optimizely.event.event_processor.ForwardingEventProcessor.process' + ) as mock_process, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision, mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ), mock.patch( + 'time.time', return_value=42 + ): + 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) + + mock_broadcast_decision.assert_called_with( + enums.NotificationTypes.DECISION, + 'feature', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': True, + 'source': 'rollout', + 'source_info': {}, + }, + ) + + # Check that impression event is not sent + self.assertEqual(0, mock_process.call_count) + + 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 """ - 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') - - mock_experiment = project_config.get_experiment_from_key('test_experiment') - mock_variation = project_config.get_variation_from_id('test_experiment', '111129') - - # Set featureEnabled property to False - 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 - )) as mock_decision, \ - mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision, \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ - mock.patch('time.time', return_value=42): - 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) - - mock_broadcast_decision.assert_called_with( - enums.NotificationTypes.DECISION, - 'feature', - 'test_user', - {}, - { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': False, - 'source': 'rollout', - 'source_info': {} - } - ) - - # Check that impression event is not sent - self.assertEqual(0, mock_process.call_count) - - 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 + 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') + + mock_experiment = project_config.get_experiment_from_key('test_experiment') + mock_variation = project_config.get_variation_from_id('test_experiment', '111129') + + # Set featureEnabled property to False + 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), + ) as mock_decision, mock.patch( + 'optimizely.event.event_processor.ForwardingEventProcessor.process' + ) as mock_process, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision, mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ), mock.patch( + 'time.time', return_value=42 + ): + 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) + + mock_broadcast_decision.assert_called_with( + enums.NotificationTypes.DECISION, + 'feature', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': False, + 'source': 'rollout', + 'source_info': {}, + }, + ) + + # Check that impression event is not sent + self.assertEqual(0, mock_process.call_count) + + 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 - )) as mock_decision, \ - mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process') as mock_process, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision, \ - mock.patch('uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c'), \ - mock.patch('time.time', return_value=42): - self.assertFalse(opt_obj.is_feature_enabled('test_feature_in_experiment', 'test_user')) + 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), + ) as mock_decision, mock.patch( + 'optimizely.event.event_processor.ForwardingEventProcessor.process' + ) as mock_process, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision, mock.patch( + 'uuid.uuid4', return_value='a68cf1ad-0393-4e18-af87-efe8f01a7c9c' + ), mock.patch( + 'time.time', return_value=42 + ): + self.assertFalse(opt_obj.is_feature_enabled('test_feature_in_experiment', 'test_user')) + + # Check that impression event is not sent + self.assertEqual(0, mock_process.call_count) + + mock_decision.assert_called_once_with(opt_obj.config_manager.get_config(), feature, 'test_user', None) + + mock_broadcast_decision.assert_called_with( + enums.NotificationTypes.DECISION, + 'feature', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': False, + 'source': 'rollout', + 'source_info': {}, + }, + ) + + # Check that impression event is not sent + self.assertEqual(0, mock_process.call_count) - # Check that impression event is not sent - self.assertEqual(0, mock_process.call_count) + def test_is_feature_enabled__invalid_object(self): + """ Test that is_feature_enabled returns False and logs error if Optimizely instance is invalid. """ - mock_decision.assert_called_once_with(opt_obj.config_manager.get_config(), feature, 'test_user', None) + class InvalidConfigManager(object): + pass - mock_broadcast_decision.assert_called_with( - enums.NotificationTypes.DECISION, - 'feature', - 'test_user', - {}, - { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': False, - 'source': 'rollout', - 'source_info': {} - } - ) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), config_manager=InvalidConfigManager()) - # Check that impression event is not sent - self.assertEqual(0, mock_process.call_count) + with mock.patch.object(opt_obj, 'logger') as mock_client_logging: + self.assertFalse(opt_obj.is_feature_enabled('test_feature_in_experiment', 'user_1')) - def test_is_feature_enabled__invalid_object(self): - """ Test that is_feature_enabled returns False and logs error if Optimizely instance is invalid. """ + mock_client_logging.error.assert_called_once_with( + 'Optimizely instance is not valid. Failing "is_feature_enabled".' + ) - class InvalidConfigManager(object): - pass + def test_is_feature_enabled__invalid_config(self): + """ Test that is_feature_enabled returns False if config is invalid. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), config_manager=InvalidConfigManager()) + opt_obj = optimizely.Optimizely('invalid_file') - with mock.patch.object(opt_obj, 'logger') as mock_client_logging: - self.assertFalse(opt_obj.is_feature_enabled('test_feature_in_experiment', 'user_1')) + with mock.patch.object(opt_obj, 'logger') as mock_client_logging, mock.patch( + 'optimizely.event_dispatcher.EventDispatcher.dispatch_event' + ) as mock_dispatch_event: + self.assertFalse(opt_obj.is_feature_enabled('test_feature_in_experiment', 'user_1')) - mock_client_logging.error.assert_called_once_with('Optimizely instance is not valid. Failing "is_feature_enabled".') + mock_client_logging.error.assert_called_once_with( + 'Invalid config. Optimizely instance is not valid. ' 'Failing "is_feature_enabled".' + ) - def test_is_feature_enabled__invalid_config(self): - """ Test that is_feature_enabled returns False if config is invalid. """ + # Check that no event is sent + self.assertEqual(0, mock_dispatch_event.call_count) - opt_obj = optimizely.Optimizely('invalid_file') + def test_get_enabled_features(self): + """ Test that get_enabled_features only returns features that are enabled for the specified user. """ - with mock.patch.object(opt_obj, 'logger') as mock_client_logging, \ - mock.patch('optimizely.event_dispatcher.EventDispatcher.dispatch_event') as mock_dispatch_event: - self.assertFalse(opt_obj.is_feature_enabled('test_feature_in_experiment', 'user_1')) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - mock_client_logging.error.assert_called_once_with('Invalid config. Optimizely instance is not valid. ' - 'Failing "is_feature_enabled".') + def side_effect(*args, **kwargs): + feature_key = args[0] + if feature_key == 'test_feature_in_experiment' or feature_key == 'test_feature_in_rollout': + return True - # Check that no event is sent - self.assertEqual(0, mock_dispatch_event.call_count) + return False - def test_get_enabled_features(self): - """ Test that get_enabled_features only returns features that are enabled for the specified user. """ + with mock.patch( + 'optimizely.optimizely.Optimizely.is_feature_enabled', side_effect=side_effect, + ) as mock_is_feature_enabled: + received_features = opt_obj.get_enabled_features('user_1') - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + expected_enabled_features = [ + 'test_feature_in_experiment', + 'test_feature_in_rollout', + ] + self.assertEqual(sorted(expected_enabled_features), sorted(received_features)) + mock_is_feature_enabled.assert_any_call('test_feature_in_experiment', 'user_1', None) + mock_is_feature_enabled.assert_any_call('test_feature_in_rollout', 'user_1', None) + mock_is_feature_enabled.assert_any_call('test_feature_in_group', 'user_1', None) + mock_is_feature_enabled.assert_any_call('test_feature_in_experiment_and_rollout', 'user_1', None) - def side_effect(*args, **kwargs): - feature_key = args[0] - if feature_key == 'test_feature_in_experiment' or feature_key == 'test_feature_in_rollout': - return True + def test_get_enabled_features__broadcasts_decision_for_each_feature(self): + """ Test that get_enabled_features only returns features that are enabled for the specified user \ + and broadcasts decision for each feature. """ - return False + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + 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') + mock_variation_2 = opt_obj.config_manager.get_config().get_variation_from_id('test_experiment', '111128') + + def side_effect(*args, **kwargs): + feature = args[1] + if feature.key == 'test_feature_in_experiment': + return decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST) + elif feature.key == 'test_feature_in_rollout': + return decision_service.Decision(mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT) + elif feature.key == 'test_feature_in_experiment_and_rollout': + return decision_service.Decision(mock_experiment, mock_variation_2, enums.DecisionSources.FEATURE_TEST,) + else: + return decision_service.Decision(mock_experiment, mock_variation_2, enums.DecisionSources.ROLLOUT) + + with mock.patch( + 'optimizely.decision_service.DecisionService.get_variation_for_feature', side_effect=side_effect, + ), mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision: + received_features = opt_obj.get_enabled_features('user_1') + + expected_enabled_features = [ + 'test_feature_in_experiment', + 'test_feature_in_rollout', + ] + + self.assertEqual(sorted(expected_enabled_features), sorted(received_features)) + + mock_broadcast_decision.assert_has_calls( + [ + mock.call( + enums.NotificationTypes.DECISION, + 'feature', + 'user_1', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': True, + 'source': 'feature-test', + 'source_info': {'experiment_key': 'test_experiment', 'variation_key': 'variation'}, + }, + ), + mock.call( + enums.NotificationTypes.DECISION, + 'feature', + 'user_1', + {}, + { + 'feature_key': 'test_feature_in_group', + 'feature_enabled': False, + 'source': 'rollout', + 'source_info': {}, + }, + ), + mock.call( + enums.NotificationTypes.DECISION, + 'feature', + 'user_1', + {}, + { + 'feature_key': 'test_feature_in_rollout', + 'feature_enabled': True, + 'source': 'rollout', + 'source_info': {}, + }, + ), + mock.call( + enums.NotificationTypes.DECISION, + 'feature', + 'user_1', + {}, + { + 'feature_key': 'test_feature_in_experiment_and_rollout', + 'feature_enabled': False, + 'source': 'feature-test', + 'source_info': {'experiment_key': 'test_experiment', 'variation_key': 'control'}, + }, + ), + ], + any_order=True, + ) - with mock.patch('optimizely.optimizely.Optimizely.is_feature_enabled', - side_effect=side_effect) as mock_is_feature_enabled: - received_features = opt_obj.get_enabled_features('user_1') + def test_get_enabled_features_invalid_user_id(self): + with mock.patch.object(self.optimizely, 'logger') as mock_client_logging: + self.assertEqual([], self.optimizely.get_enabled_features(1.2)) - expected_enabled_features = ['test_feature_in_experiment', 'test_feature_in_rollout'] - self.assertEqual(sorted(expected_enabled_features), sorted(received_features)) - mock_is_feature_enabled.assert_any_call('test_feature_in_experiment', 'user_1', None) - mock_is_feature_enabled.assert_any_call('test_feature_in_rollout', 'user_1', None) - mock_is_feature_enabled.assert_any_call('test_feature_in_group', 'user_1', None) - mock_is_feature_enabled.assert_any_call('test_feature_in_experiment_and_rollout', 'user_1', None) + mock_client_logging.error.assert_called_once_with('Provided "user_id" is in an invalid format.') - def test_get_enabled_features__broadcasts_decision_for_each_feature(self): - """ Test that get_enabled_features only returns features that are enabled for the specified user \ - and broadcasts decision for each feature. """ + 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 + ) as mock_validator: + self.assertEqual( + [], self.optimizely.get_enabled_features('test_user', attributes='invalid'), + ) - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - 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') - mock_variation_2 = opt_obj.config_manager.get_config().get_variation_from_id('test_experiment', '111128') - - def side_effect(*args, **kwargs): - feature = args[1] - if feature.key == 'test_feature_in_experiment': - return decision_service.Decision( - mock_experiment, mock_variation, enums.DecisionSources.FEATURE_TEST - ) - elif feature.key == 'test_feature_in_rollout': - return decision_service.Decision( - mock_experiment, mock_variation, enums.DecisionSources.ROLLOUT - ) - elif feature.key == 'test_feature_in_experiment_and_rollout': - return decision_service.Decision( - mock_experiment, mock_variation_2, enums.DecisionSources.FEATURE_TEST - ) - else: - return decision_service.Decision( - mock_experiment, mock_variation_2, enums.DecisionSources.ROLLOUT - ) - - with mock.patch('optimizely.decision_service.DecisionService.get_variation_for_feature', - side_effect=side_effect),\ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') \ - as mock_broadcast_decision: - received_features = opt_obj.get_enabled_features('user_1') - - expected_enabled_features = ['test_feature_in_experiment', 'test_feature_in_rollout'] - - self.assertEqual(sorted(expected_enabled_features), sorted(received_features)) - - mock_broadcast_decision.assert_has_calls([ - mock.call( - enums.NotificationTypes.DECISION, - 'feature', - 'user_1', - {}, - { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': True, - 'source': 'feature-test', - 'source_info': { - 'experiment_key': 'test_experiment', - 'variation_key': 'variation' - } - } - ), - mock.call( - enums.NotificationTypes.DECISION, - 'feature', - 'user_1', - {}, - { - 'feature_key': 'test_feature_in_group', - 'feature_enabled': False, - 'source': 'rollout', - 'source_info': {} - } - ), - mock.call( - enums.NotificationTypes.DECISION, - 'feature', - 'user_1', - {}, - { - 'feature_key': 'test_feature_in_rollout', - 'feature_enabled': True, - 'source': 'rollout', - 'source_info': {} - } - ), - mock.call( - enums.NotificationTypes.DECISION, - 'feature', - 'user_1', - {}, - { - 'feature_key': 'test_feature_in_experiment_and_rollout', - 'feature_enabled': False, - 'source': 'feature-test', - 'source_info': { - 'experiment_key': 'test_experiment', - 'variation_key': 'control' - } - } - ) - ], any_order=True) - - def test_get_enabled_features_invalid_user_id(self): - with mock.patch.object(self.optimizely, 'logger') as mock_client_logging: - self.assertEqual([], self.optimizely.get_enabled_features(1.2)) - - mock_client_logging.error.assert_called_once_with('Provided "user_id" is in an invalid format.') - - 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) as mock_validator: - self.assertEqual([], self.optimizely.get_enabled_features('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.') - - def test_get_enabled_features__invalid_object(self): - """ Test that get_enabled_features returns empty list if Optimizely instance is invalid. """ - - class InvalidConfigManager(object): - pass - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), config_manager=InvalidConfigManager()) - - with mock.patch.object(opt_obj, 'logger') as mock_client_logging: - self.assertEqual([], opt_obj.get_enabled_features('test_user')) - - mock_client_logging.error.assert_called_once_with('Optimizely instance is not valid. ' - 'Failing "get_enabled_features".') - - def test_get_enabled_features__invalid_config(self): - """ Test that get_enabled_features returns empty list if config is invalid. """ - - opt_obj = optimizely.Optimizely('invalid_file') - - with mock.patch.object(opt_obj, 'logger') as mock_client_logging: - self.assertEqual([], opt_obj.get_enabled_features('user_1')) - - mock_client_logging.error.assert_called_once_with('Invalid config. Optimizely instance is not valid. ' - 'Failing "get_enabled_features".') - - def test_get_feature_variable_boolean(self): - """ Test that get_feature_variable_boolean returns Boolean value as expected \ + mock_validator.assert_called_once_with('invalid') + mock_client_logging.error.assert_called_once_with('Provided attributes are in an invalid format.') + + def test_get_enabled_features__invalid_object(self): + """ Test that get_enabled_features returns empty list if Optimizely instance is invalid. """ + + class InvalidConfigManager(object): + pass + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), config_manager=InvalidConfigManager()) + + with mock.patch.object(opt_obj, 'logger') as mock_client_logging: + self.assertEqual([], opt_obj.get_enabled_features('test_user')) + + mock_client_logging.error.assert_called_once_with( + 'Optimizely instance is not valid. ' 'Failing "get_enabled_features".' + ) + + def test_get_enabled_features__invalid_config(self): + """ Test that get_enabled_features returns empty list if config is invalid. """ + + opt_obj = optimizely.Optimizely('invalid_file') + + with mock.patch.object(opt_obj, 'logger') as mock_client_logging: + self.assertEqual([], opt_obj.get_enabled_features('user_1')) + + mock_client_logging.error.assert_called_once_with( + 'Invalid config. Optimizely instance is not valid. ' 'Failing "get_enabled_features".' + ) + + def test_get_feature_variable_boolean(self): + """ Test that get_feature_variable_boolean returns Boolean value as expected \ and broadcasts decision with proper parameters. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - 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)), \ - mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: - self.assertTrue(opt_obj.get_feature_variable_boolean('test_feature_in_experiment', 'is_working', 'test_user')) - - mock_config_logging.info.assert_called_once_with( - 'Value for variable "is_working" for variation "variation" is "true".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {}, - { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': True, - 'source': 'feature-test', - 'variable_key': 'is_working', - 'variable_value': True, - 'variable_type': 'boolean', - 'source_info': { - 'experiment_key': 'test_experiment', - 'variation_key': 'variation' - } - } - ) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + 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), + ), mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision: + self.assertTrue( + opt_obj.get_feature_variable_boolean('test_feature_in_experiment', 'is_working', 'test_user') + ) + + mock_config_logging.info.assert_called_once_with( + 'Value for variable "is_working" for variation "variation" is "true".' + ) - def test_get_feature_variable_double(self): - """ Test that get_feature_variable_double returns Double value as expected \ + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': True, + 'source': 'feature-test', + 'variable_key': 'is_working', + 'variable_value': True, + 'variable_type': 'boolean', + 'source_info': {'experiment_key': 'test_experiment', 'variation_key': 'variation'}, + }, + ) + + def test_get_feature_variable_double(self): + """ Test that get_feature_variable_double returns Double value as expected \ and broadcasts decision with proper parameters. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - 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)), \ - mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: - self.assertEqual(10.02, opt_obj.get_feature_variable_double('test_feature_in_experiment', 'cost', 'test_user')) - - mock_config_logging.info.assert_called_once_with( - 'Value for variable "cost" for variation "variation" is "10.02".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {}, - { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': True, - 'source': 'feature-test', - 'variable_key': 'cost', - 'variable_value': 10.02, - 'variable_type': 'double', - 'source_info': { - 'experiment_key': 'test_experiment', - 'variation_key': 'variation' - } - } - ) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + 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), + ), mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision: + self.assertEqual( + 10.02, opt_obj.get_feature_variable_double('test_feature_in_experiment', 'cost', 'test_user'), + ) + + mock_config_logging.info.assert_called_once_with( + 'Value for variable "cost" for variation "variation" is "10.02".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': True, + 'source': 'feature-test', + 'variable_key': 'cost', + 'variable_value': 10.02, + 'variable_type': 'double', + 'source_info': {'experiment_key': 'test_experiment', 'variation_key': 'variation'}, + }, + ) - def test_get_feature_variable_integer(self): - """ Test that get_feature_variable_integer returns Integer value as expected \ + def test_get_feature_variable_integer(self): + """ Test that get_feature_variable_integer returns Integer value as expected \ and broadcasts decision with proper parameters. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - 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)), \ - mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: - self.assertEqual(4243, opt_obj.get_feature_variable_integer('test_feature_in_experiment', 'count', 'test_user')) - - mock_config_logging.info.assert_called_once_with( - 'Value for variable "count" for variation "variation" is "4243".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {}, - { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': True, - 'source': 'feature-test', - 'variable_key': 'count', - 'variable_value': 4243, - 'variable_type': 'integer', - 'source_info': { - 'experiment_key': 'test_experiment', - 'variation_key': 'variation' - } - } - ) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + 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), + ), mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision: + self.assertEqual( + 4243, opt_obj.get_feature_variable_integer('test_feature_in_experiment', 'count', 'test_user'), + ) + + mock_config_logging.info.assert_called_once_with( + 'Value for variable "count" for variation "variation" is "4243".' + ) - def test_get_feature_variable_string(self): - """ Test that get_feature_variable_string returns String value as expected \ + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': True, + 'source': 'feature-test', + 'variable_key': 'count', + 'variable_value': 4243, + 'variable_type': 'integer', + 'source_info': {'experiment_key': 'test_experiment', 'variation_key': 'variation'}, + }, + ) + + def test_get_feature_variable_string(self): + """ Test that get_feature_variable_string returns String value as expected \ and broadcasts decision with proper parameters. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - 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)), \ - mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: - self.assertEqual( - 'staging', - opt_obj.get_feature_variable_string('test_feature_in_experiment', 'environment', 'test_user') - ) - - mock_config_logging.info.assert_called_once_with( - 'Value for variable "environment" for variation "variation" is "staging".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {}, - { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': True, - 'source': 'feature-test', - 'variable_key': 'environment', - 'variable_value': 'staging', - 'variable_type': 'string', - 'source_info': { - 'experiment_key': 'test_experiment', - 'variation_key': 'variation' - } - } - ) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + 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), + ), mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision: + self.assertEqual( + 'staging', + opt_obj.get_feature_variable_string('test_feature_in_experiment', 'environment', 'test_user'), + ) + + mock_config_logging.info.assert_called_once_with( + 'Value for variable "environment" for variation "variation" is "staging".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': True, + 'source': 'feature-test', + 'variable_key': 'environment', + 'variable_value': 'staging', + 'variable_type': 'string', + 'source_info': {'experiment_key': 'test_experiment', 'variation_key': 'variation'}, + }, + ) - def test_get_feature_variable(self): - """ Test that get_feature_variable returns variable value as expected \ + def test_get_feature_variable(self): + """ Test that get_feature_variable returns variable value as expected \ and broadcasts decision with proper parameters. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - 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') - # 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)), \ - mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: - self.assertTrue(opt_obj.get_feature_variable('test_feature_in_experiment', 'is_working', 'test_user')) - - mock_config_logging.info.assert_called_once_with( - 'Value for variable "is_working" for variation "variation" is "true".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {}, - { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': True, - 'source': 'feature-test', - 'variable_key': 'is_working', - 'variable_value': True, - 'variable_type': 'boolean', - 'source_info': { - 'experiment_key': 'test_experiment', - 'variation_key': 'variation' - } - } - ) - # 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)), \ - mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: - self.assertEqual(10.02, opt_obj.get_feature_variable('test_feature_in_experiment', 'cost', 'test_user')) - - mock_config_logging.info.assert_called_once_with( - 'Value for variable "cost" for variation "variation" is "10.02".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {}, - { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': True, - 'source': 'feature-test', - 'variable_key': 'cost', - 'variable_value': 10.02, - 'variable_type': 'double', - 'source_info': { - 'experiment_key': 'test_experiment', - 'variation_key': 'variation' - } - } - ) - # 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)), \ - mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: - self.assertEqual(4243, opt_obj.get_feature_variable('test_feature_in_experiment', 'count', 'test_user')) - - mock_config_logging.info.assert_called_once_with( - 'Value for variable "count" for variation "variation" is "4243".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {}, - { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': True, - 'source': 'feature-test', - 'variable_key': 'count', - 'variable_value': 4243, - 'variable_type': 'integer', - 'source_info': { - 'experiment_key': 'test_experiment', - 'variation_key': 'variation' - } - } - ) - # 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)), \ - mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: - self.assertEqual( - 'staging', - opt_obj.get_feature_variable('test_feature_in_experiment', 'environment', 'test_user') - ) - - mock_config_logging.info.assert_called_once_with( - 'Value for variable "environment" for variation "variation" is "staging".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {}, - { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': True, - 'source': 'feature-test', - 'variable_key': 'environment', - 'variable_value': 'staging', - 'variable_type': 'string', - 'source_info': { - 'experiment_key': 'test_experiment', - 'variation_key': 'variation' - } - } - ) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + 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') + # 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), + ), mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision: + self.assertTrue(opt_obj.get_feature_variable('test_feature_in_experiment', 'is_working', 'test_user')) + + mock_config_logging.info.assert_called_once_with( + 'Value for variable "is_working" for variation "variation" is "true".' + ) - def test_get_feature_variable_boolean_for_feature_in_rollout(self): - """ Test that get_feature_variable_boolean returns Boolean value as expected \ + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': True, + 'source': 'feature-test', + 'variable_key': 'is_working', + 'variable_value': True, + 'variable_type': 'boolean', + 'source_info': {'experiment_key': 'test_experiment', 'variation_key': 'variation'}, + }, + ) + # 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), + ), mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision: + self.assertEqual( + 10.02, opt_obj.get_feature_variable('test_feature_in_experiment', 'cost', 'test_user'), + ) + + mock_config_logging.info.assert_called_once_with( + 'Value for variable "cost" for variation "variation" is "10.02".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': True, + 'source': 'feature-test', + 'variable_key': 'cost', + 'variable_value': 10.02, + 'variable_type': 'double', + 'source_info': {'experiment_key': 'test_experiment', 'variation_key': 'variation'}, + }, + ) + # 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), + ), mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision: + self.assertEqual( + 4243, opt_obj.get_feature_variable('test_feature_in_experiment', 'count', 'test_user'), + ) + + mock_config_logging.info.assert_called_once_with( + 'Value for variable "count" for variation "variation" is "4243".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': True, + 'source': 'feature-test', + 'variable_key': 'count', + 'variable_value': 4243, + 'variable_type': 'integer', + 'source_info': {'experiment_key': 'test_experiment', 'variation_key': 'variation'}, + }, + ) + # 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), + ), mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision: + self.assertEqual( + 'staging', opt_obj.get_feature_variable('test_feature_in_experiment', 'environment', 'test_user'), + ) + + mock_config_logging.info.assert_called_once_with( + 'Value for variable "environment" for variation "variation" is "staging".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': True, + 'source': 'feature-test', + 'variable_key': 'environment', + 'variable_value': 'staging', + 'variable_type': 'string', + 'source_info': {'experiment_key': 'test_experiment', 'variation_key': 'variation'}, + }, + ) + + def test_get_feature_variable_boolean_for_feature_in_rollout(self): + """ Test that get_feature_variable_boolean returns Boolean value as expected \ and broadcasts decision with proper parameters. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - mock_experiment = opt_obj.config_manager.get_config().get_experiment_from_key('211127') - mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('211127', '211129') - 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)), \ - mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: - self.assertTrue(opt_obj.get_feature_variable_boolean('test_feature_in_rollout', 'is_running', 'test_user', - attributes=user_attributes)) - - mock_config_logging.info.assert_called_once_with( - 'Value for variable "is_running" for variation "211129" is "true".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {'test_attribute': 'test_value'}, - { - 'feature_key': 'test_feature_in_rollout', - 'feature_enabled': True, - 'source': 'rollout', - 'variable_key': 'is_running', - 'variable_value': True, - 'variable_type': 'boolean', - 'source_info': {} - } - ) - - def test_get_feature_variable_double_for_feature_in_rollout(self): - """ Test that get_feature_variable_double returns Double value as expected \ + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + mock_experiment = opt_obj.config_manager.get_config().get_experiment_from_key('211127') + mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('211127', '211129') + 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), + ), mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision: + self.assertTrue( + opt_obj.get_feature_variable_boolean( + 'test_feature_in_rollout', 'is_running', 'test_user', attributes=user_attributes, + ) + ) + + mock_config_logging.info.assert_called_once_with( + 'Value for variable "is_running" for variation "211129" is "true".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {'test_attribute': 'test_value'}, + { + 'feature_key': 'test_feature_in_rollout', + 'feature_enabled': True, + 'source': 'rollout', + 'variable_key': 'is_running', + 'variable_value': True, + 'variable_type': 'boolean', + 'source_info': {}, + }, + ) + + def test_get_feature_variable_double_for_feature_in_rollout(self): + """ Test that get_feature_variable_double returns Double value as expected \ and broadcasts decision with proper parameters. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - mock_experiment = opt_obj.config_manager.get_config().get_experiment_from_key('211127') - mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('211127', '211129') - 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)), \ - mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: - self.assertTrue(opt_obj.get_feature_variable_double('test_feature_in_rollout', 'price', 'test_user', - attributes=user_attributes)) - - mock_config_logging.info.assert_called_once_with( - 'Value for variable "price" for variation "211129" is "39.99".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {'test_attribute': 'test_value'}, - { - 'feature_key': 'test_feature_in_rollout', - 'feature_enabled': True, - 'source': 'rollout', - 'variable_key': 'price', - 'variable_value': 39.99, - 'variable_type': 'double', - 'source_info': {} - } - ) - - def test_get_feature_variable_integer_for_feature_in_rollout(self): - """ Test that get_feature_variable_integer returns Double value as expected \ + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + mock_experiment = opt_obj.config_manager.get_config().get_experiment_from_key('211127') + mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('211127', '211129') + 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), + ), mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision: + self.assertTrue( + opt_obj.get_feature_variable_double( + 'test_feature_in_rollout', 'price', 'test_user', attributes=user_attributes, + ) + ) + + mock_config_logging.info.assert_called_once_with( + 'Value for variable "price" for variation "211129" is "39.99".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {'test_attribute': 'test_value'}, + { + 'feature_key': 'test_feature_in_rollout', + 'feature_enabled': True, + 'source': 'rollout', + 'variable_key': 'price', + 'variable_value': 39.99, + 'variable_type': 'double', + 'source_info': {}, + }, + ) + + def test_get_feature_variable_integer_for_feature_in_rollout(self): + """ Test that get_feature_variable_integer returns Double value as expected \ and broadcasts decision with proper parameters. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - mock_experiment = opt_obj.config_manager.get_config().get_experiment_from_key('211127') - mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('211127', '211129') - 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)), \ - mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: - self.assertTrue(opt_obj.get_feature_variable_integer('test_feature_in_rollout', 'count', 'test_user', - attributes=user_attributes)) - - mock_config_logging.info.assert_called_once_with( - 'Value for variable "count" for variation "211129" is "399".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {'test_attribute': 'test_value'}, - { - 'feature_key': 'test_feature_in_rollout', - 'feature_enabled': True, - 'source': 'rollout', - 'variable_key': 'count', - 'variable_value': 399, - 'variable_type': 'integer', - 'source_info': {} - } - ) - - def test_get_feature_variable_string_for_feature_in_rollout(self): - """ Test that get_feature_variable_double returns Double value as expected + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + mock_experiment = opt_obj.config_manager.get_config().get_experiment_from_key('211127') + mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('211127', '211129') + 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), + ), mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision: + self.assertTrue( + opt_obj.get_feature_variable_integer( + 'test_feature_in_rollout', 'count', 'test_user', attributes=user_attributes, + ) + ) + + mock_config_logging.info.assert_called_once_with('Value for variable "count" for variation "211129" is "399".') + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {'test_attribute': 'test_value'}, + { + 'feature_key': 'test_feature_in_rollout', + 'feature_enabled': True, + 'source': 'rollout', + 'variable_key': 'count', + 'variable_value': 399, + 'variable_type': 'integer', + 'source_info': {}, + }, + ) + + def test_get_feature_variable_string_for_feature_in_rollout(self): + """ Test that get_feature_variable_double returns Double value as expected and broadcasts decision with proper parameters. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - mock_experiment = opt_obj.config_manager.get_config().get_experiment_from_key('211127') - mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('211127', '211129') - 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)), \ - mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: - self.assertTrue(opt_obj.get_feature_variable_string('test_feature_in_rollout', 'message', 'test_user', - attributes=user_attributes)) - - mock_config_logging.info.assert_called_once_with( - 'Value for variable "message" for variation "211129" is "Hello audience".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {'test_attribute': 'test_value'}, - { - 'feature_key': 'test_feature_in_rollout', - 'feature_enabled': True, - 'source': 'rollout', - 'variable_key': 'message', - 'variable_value': 'Hello audience', - 'variable_type': 'string', - 'source_info': {} - } - ) - - def test_get_feature_variable_for_feature_in_rollout(self): - """ Test that get_feature_variable returns value as expected and broadcasts decision with proper parameters. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - mock_experiment = opt_obj.config_manager.get_config().get_experiment_from_key('211127') - mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('211127', '211129') - user_attributes = {'test_attribute': 'test_value'} - - # Boolean - with mock.patch('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.config_manager.get_config(), 'logger') as mock_config_logging, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: - self.assertTrue(opt_obj.get_feature_variable('test_feature_in_rollout', 'is_running', 'test_user', - attributes=user_attributes)) - - mock_config_logging.info.assert_called_once_with( - 'Value for variable "is_running" for variation "211129" is "true".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {'test_attribute': 'test_value'}, - { - 'feature_key': 'test_feature_in_rollout', - 'feature_enabled': True, - 'source': 'rollout', - 'variable_key': 'is_running', - 'variable_value': True, - 'variable_type': 'boolean', - 'source_info': {} - } - ) - # Double - with mock.patch('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.config_manager.get_config(), 'logger') as mock_config_logging, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: - self.assertTrue(opt_obj.get_feature_variable('test_feature_in_rollout', 'price', 'test_user', - attributes=user_attributes)) - - mock_config_logging.info.assert_called_once_with( - 'Value for variable "price" for variation "211129" is "39.99".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {'test_attribute': 'test_value'}, - { - 'feature_key': 'test_feature_in_rollout', - 'feature_enabled': True, - 'source': 'rollout', - 'variable_key': 'price', - 'variable_value': 39.99, - 'variable_type': 'double', - 'source_info': {} - } - ) - # Integer - with mock.patch('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.config_manager.get_config(), 'logger') as mock_config_logging, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: - self.assertTrue(opt_obj.get_feature_variable('test_feature_in_rollout', 'count', 'test_user', - attributes=user_attributes)) - - mock_config_logging.info.assert_called_once_with( - 'Value for variable "count" for variation "211129" is "399".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {'test_attribute': 'test_value'}, - { - 'feature_key': 'test_feature_in_rollout', - 'feature_enabled': True, - 'source': 'rollout', - 'variable_key': 'count', - 'variable_value': 399, - 'variable_type': 'integer', - 'source_info': {} - } - ) - # String - with mock.patch('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.config_manager.get_config(), 'logger') as mock_config_logging, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: - self.assertTrue(opt_obj.get_feature_variable('test_feature_in_rollout', 'message', 'test_user', - attributes=user_attributes)) - - mock_config_logging.info.assert_called_once_with( - 'Value for variable "message" for variation "211129" is "Hello audience".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {'test_attribute': 'test_value'}, - { - 'feature_key': 'test_feature_in_rollout', - 'feature_enabled': True, - 'source': 'rollout', - 'variable_key': 'message', - 'variable_value': 'Hello audience', - 'variable_type': 'string', - 'source_info': {} - } - ) - - 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)) - 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') - - # Empty variable usage map for the mocked variation - opt_obj.config_manager.get_config().variation_variable_usage_map['111129'] = None - - # 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)), \ - mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logger: - self.assertTrue(opt_obj.get_feature_variable_boolean('test_feature_in_experiment', 'is_working', 'test_user')) - - mock_config_logger.info.assert_called_once_with( - 'Variable "is_working" is not used in variation "variation". Assigning default value "true".' - ) - mock_config_logger.info.reset_mock() - - # 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)), \ - mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logger: - self.assertEqual(10.99, - opt_obj.get_feature_variable_double('test_feature_in_experiment', 'cost', 'test_user')) - - mock_config_logger.info.assert_called_once_with( - 'Variable "cost" is not used in variation "variation". Assigning default value "10.99".' - ) - mock_config_logger.info.reset_mock() - - # 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)), \ - mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logger: - self.assertEqual(999, - opt_obj.get_feature_variable_integer('test_feature_in_experiment', 'count', 'test_user')) - - mock_config_logger.info.assert_called_once_with( - 'Variable "count" is not used in variation "variation". Assigning default value "999".' - ) - mock_config_logger.info.reset_mock() - - # 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)), \ - mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logger: - self.assertEqual('devel', - opt_obj.get_feature_variable_string('test_feature_in_experiment', 'environment', 'test_user')) - - mock_config_logger.info.assert_called_once_with( - 'Variable "environment" is not used in variation "variation". Assigning default value "devel".' - ) - mock_config_logger.info.reset_mock() - - # 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)), \ - mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logger: - self.assertTrue(opt_obj.get_feature_variable('test_feature_in_experiment', 'is_working', 'test_user')) - - mock_config_logger.info.assert_called_once_with( - 'Variable "is_working" is not used in variation "variation". Assigning default value "true".' - ) - mock_config_logger.info.reset_mock() - - with mock.patch('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.config_manager.get_config(), 'logger') as mock_config_logger: - self.assertEqual(10.99, - opt_obj.get_feature_variable('test_feature_in_experiment', 'cost', 'test_user')) - - mock_config_logger.info.assert_called_once_with( - 'Variable "cost" is not used in variation "variation". Assigning default value "10.99".' - ) - mock_config_logger.info.reset_mock() - - with mock.patch('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.config_manager.get_config(), 'logger') as mock_config_logger: - self.assertEqual(999, - opt_obj.get_feature_variable('test_feature_in_experiment', 'count', 'test_user')) - - mock_config_logger.info.assert_called_once_with( - 'Variable "count" is not used in variation "variation". Assigning default value "999".' - ) - mock_config_logger.info.reset_mock() - - with mock.patch('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.config_manager.get_config(), 'logger') as mock_config_logger: - self.assertEqual('devel', - opt_obj.get_feature_variable('test_feature_in_experiment', 'environment', 'test_user')) - - mock_config_logger.info.assert_called_once_with( - 'Variable "environment" is not used in variation "variation". Assigning default value "devel".' - ) - mock_config_logger.info.reset_mock() - - def test_get_feature_variable__returns_default_value_if_no_variation(self): - """ Test that get_feature_variable_* returns default value if no variation \ + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + mock_experiment = opt_obj.config_manager.get_config().get_experiment_from_key('211127') + mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('211127', '211129') + 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), + ), mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logging, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision: + self.assertTrue( + opt_obj.get_feature_variable_string( + 'test_feature_in_rollout', 'message', 'test_user', attributes=user_attributes, + ) + ) + + mock_config_logging.info.assert_called_once_with( + 'Value for variable "message" for variation "211129" is "Hello audience".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {'test_attribute': 'test_value'}, + { + 'feature_key': 'test_feature_in_rollout', + 'feature_enabled': True, + 'source': 'rollout', + 'variable_key': 'message', + 'variable_value': 'Hello audience', + 'variable_type': 'string', + 'source_info': {}, + }, + ) + + def test_get_feature_variable_for_feature_in_rollout(self): + """ Test that get_feature_variable returns value as expected and broadcasts decision with proper parameters. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + mock_experiment = opt_obj.config_manager.get_config().get_experiment_from_key('211127') + mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('211127', '211129') + user_attributes = {'test_attribute': 'test_value'} + + # Boolean + with mock.patch( + '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.config_manager.get_config(), 'logger') as mock_config_logging, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision: + self.assertTrue( + opt_obj.get_feature_variable( + 'test_feature_in_rollout', 'is_running', 'test_user', attributes=user_attributes, + ) + ) + + mock_config_logging.info.assert_called_once_with( + 'Value for variable "is_running" for variation "211129" is "true".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {'test_attribute': 'test_value'}, + { + 'feature_key': 'test_feature_in_rollout', + 'feature_enabled': True, + 'source': 'rollout', + 'variable_key': 'is_running', + 'variable_value': True, + 'variable_type': 'boolean', + 'source_info': {}, + }, + ) + # Double + with mock.patch( + '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.config_manager.get_config(), 'logger') as mock_config_logging, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision: + self.assertTrue( + opt_obj.get_feature_variable( + 'test_feature_in_rollout', 'price', 'test_user', attributes=user_attributes, + ) + ) + + mock_config_logging.info.assert_called_once_with( + 'Value for variable "price" for variation "211129" is "39.99".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {'test_attribute': 'test_value'}, + { + 'feature_key': 'test_feature_in_rollout', + 'feature_enabled': True, + 'source': 'rollout', + 'variable_key': 'price', + 'variable_value': 39.99, + 'variable_type': 'double', + 'source_info': {}, + }, + ) + # Integer + with mock.patch( + '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.config_manager.get_config(), 'logger') as mock_config_logging, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision: + self.assertTrue( + opt_obj.get_feature_variable( + 'test_feature_in_rollout', 'count', 'test_user', attributes=user_attributes, + ) + ) + + mock_config_logging.info.assert_called_once_with('Value for variable "count" for variation "211129" is "399".') + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {'test_attribute': 'test_value'}, + { + 'feature_key': 'test_feature_in_rollout', + 'feature_enabled': True, + 'source': 'rollout', + 'variable_key': 'count', + 'variable_value': 399, + 'variable_type': 'integer', + 'source_info': {}, + }, + ) + # String + with mock.patch( + '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.config_manager.get_config(), 'logger') as mock_config_logging, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision: + self.assertTrue( + opt_obj.get_feature_variable( + 'test_feature_in_rollout', 'message', 'test_user', attributes=user_attributes, + ) + ) + + mock_config_logging.info.assert_called_once_with( + 'Value for variable "message" for variation "211129" is "Hello audience".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {'test_attribute': 'test_value'}, + { + 'feature_key': 'test_feature_in_rollout', + 'feature_enabled': True, + 'source': 'rollout', + 'variable_key': 'message', + 'variable_value': 'Hello audience', + 'variable_type': 'string', + 'source_info': {}, + }, + ) + + 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)) + 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') + + # Empty variable usage map for the mocked variation + opt_obj.config_manager.get_config().variation_variable_usage_map['111129'] = None + + # 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), + ), mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logger: + self.assertTrue( + opt_obj.get_feature_variable_boolean('test_feature_in_experiment', 'is_working', 'test_user') + ) + + mock_config_logger.info.assert_called_once_with( + 'Variable "is_working" is not used in variation "variation". Assigning default value "true".' + ) + mock_config_logger.info.reset_mock() + + # 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), + ), mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logger: + self.assertEqual( + 10.99, opt_obj.get_feature_variable_double('test_feature_in_experiment', 'cost', 'test_user'), + ) + + mock_config_logger.info.assert_called_once_with( + 'Variable "cost" is not used in variation "variation". Assigning default value "10.99".' + ) + mock_config_logger.info.reset_mock() + + # 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), + ), mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logger: + self.assertEqual( + 999, opt_obj.get_feature_variable_integer('test_feature_in_experiment', 'count', 'test_user'), + ) + + mock_config_logger.info.assert_called_once_with( + 'Variable "count" is not used in variation "variation". Assigning default value "999".' + ) + mock_config_logger.info.reset_mock() + + # 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), + ), mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logger: + self.assertEqual( + 'devel', opt_obj.get_feature_variable_string('test_feature_in_experiment', 'environment', 'test_user'), + ) + + mock_config_logger.info.assert_called_once_with( + 'Variable "environment" is not used in variation "variation". Assigning default value "devel".' + ) + mock_config_logger.info.reset_mock() + + # 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), + ), mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logger: + self.assertTrue(opt_obj.get_feature_variable('test_feature_in_experiment', 'is_working', 'test_user')) + + mock_config_logger.info.assert_called_once_with( + 'Variable "is_working" is not used in variation "variation". Assigning default value "true".' + ) + mock_config_logger.info.reset_mock() + + with mock.patch( + '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.config_manager.get_config(), 'logger') as mock_config_logger: + self.assertEqual( + 10.99, opt_obj.get_feature_variable('test_feature_in_experiment', 'cost', 'test_user'), + ) + + mock_config_logger.info.assert_called_once_with( + 'Variable "cost" is not used in variation "variation". Assigning default value "10.99".' + ) + mock_config_logger.info.reset_mock() + + with mock.patch( + '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.config_manager.get_config(), 'logger') as mock_config_logger: + self.assertEqual( + 999, opt_obj.get_feature_variable('test_feature_in_experiment', 'count', 'test_user'), + ) + + mock_config_logger.info.assert_called_once_with( + 'Variable "count" is not used in variation "variation". Assigning default value "999".' + ) + mock_config_logger.info.reset_mock() + + with mock.patch( + '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.config_manager.get_config(), 'logger') as mock_config_logger: + self.assertEqual( + 'devel', opt_obj.get_feature_variable('test_feature_in_experiment', 'environment', 'test_user'), + ) + + mock_config_logger.info.assert_called_once_with( + 'Variable "environment" is not used in variation "variation". Assigning default value "devel".' + ) + mock_config_logger.info.reset_mock() + + def test_get_feature_variable__returns_default_value_if_no_variation(self): + """ Test that get_feature_variable_* returns default value if no variation \ and broadcasts decision with proper parameters. """ - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - - # Boolean - with mock.patch('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: - self.assertTrue(opt_obj.get_feature_variable_boolean('test_feature_in_experiment', 'is_working', 'test_user')) - - mock_client_logger.info.assert_called_once_with( - 'User "test_user" is not in any variation or rollout rule. ' - 'Returning default value for variable "is_working" of feature flag "test_feature_in_experiment".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {}, - { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': False, - 'source': 'rollout', - 'variable_key': 'is_working', - 'variable_value': True, - 'variable_type': 'boolean', - 'source_info': {} - } - ) - - mock_client_logger.info.reset_mock() - - # Double - with mock.patch('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: - self.assertEqual(10.99, - opt_obj.get_feature_variable_double('test_feature_in_experiment', 'cost', 'test_user')) - - mock_client_logger.info.assert_called_once_with( - 'User "test_user" is not in any variation or rollout rule. ' - 'Returning default value for variable "cost" of feature flag "test_feature_in_experiment".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {}, - { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': False, - 'source': 'rollout', - 'variable_key': 'cost', - 'variable_value': 10.99, - 'variable_type': 'double', - 'source_info': {} - } - ) - - mock_client_logger.info.reset_mock() - - # Integer - with mock.patch('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: - self.assertEqual(999, - opt_obj.get_feature_variable_integer('test_feature_in_experiment', 'count', 'test_user')) - - mock_client_logger.info.assert_called_once_with( - 'User "test_user" is not in any variation or rollout rule. ' - 'Returning default value for variable "count" of feature flag "test_feature_in_experiment".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {}, - { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': False, - 'source': 'rollout', - 'variable_key': 'count', - 'variable_value': 999, - 'variable_type': 'integer', - 'source_info': {} - } - ) - - mock_client_logger.info.reset_mock() - - # String - with mock.patch('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: - self.assertEqual('devel', - opt_obj.get_feature_variable_string('test_feature_in_experiment', 'environment', 'test_user')) - - mock_client_logger.info.assert_called_once_with( - 'User "test_user" is not in any variation or rollout rule. ' - 'Returning default value for variable "environment" of feature flag "test_feature_in_experiment".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {}, - { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': False, - 'source': 'rollout', - 'variable_key': 'environment', - 'variable_value': 'devel', - 'variable_type': 'string', - 'source_info': {} - } - ) - - mock_client_logger.info.reset_mock() - - # Non-typed - with mock.patch('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: - self.assertTrue(opt_obj.get_feature_variable('test_feature_in_experiment', 'is_working', 'test_user')) - - mock_client_logger.info.assert_called_once_with( - 'User "test_user" is not in any variation or rollout rule. ' - 'Returning default value for variable "is_working" of feature flag "test_feature_in_experiment".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {}, - { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': False, - 'source': 'rollout', - 'variable_key': 'is_working', - 'variable_value': True, - 'variable_type': 'boolean', - 'source_info': {} - } - ) - - 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)), \ - mock.patch.object(opt_obj, 'logger') as mock_client_logger, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: - self.assertEqual(10.99, - opt_obj.get_feature_variable('test_feature_in_experiment', 'cost', 'test_user')) - - mock_client_logger.info.assert_called_once_with( - 'User "test_user" is not in any variation or rollout rule. ' - 'Returning default value for variable "cost" of feature flag "test_feature_in_experiment".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {}, - { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': False, - 'source': 'rollout', - 'variable_key': 'cost', - 'variable_value': 10.99, - 'variable_type': 'double', - 'source_info': {} - } - ) - - 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)), \ - mock.patch.object(opt_obj, 'logger') as mock_client_logger, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: - self.assertEqual(999, - opt_obj.get_feature_variable('test_feature_in_experiment', 'count', 'test_user')) - - mock_client_logger.info.assert_called_once_with( - 'User "test_user" is not in any variation or rollout rule. ' - 'Returning default value for variable "count" of feature flag "test_feature_in_experiment".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {}, - { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': False, - 'source': 'rollout', - 'variable_key': 'count', - 'variable_value': 999, - 'variable_type': 'integer', - 'source_info': {} - } - ) - - 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)), \ - mock.patch.object(opt_obj, 'logger') as mock_client_logger, \ - mock.patch('optimizely.notification_center.NotificationCenter.send_notifications') as mock_broadcast_decision: - self.assertEqual('devel', - opt_obj.get_feature_variable('test_feature_in_experiment', 'environment', 'test_user')) - - mock_client_logger.info.assert_called_once_with( - 'User "test_user" is not in any variation or rollout rule. ' - 'Returning default value for variable "environment" of feature flag "test_feature_in_experiment".' - ) - - mock_broadcast_decision.assert_called_once_with( - enums.NotificationTypes.DECISION, - 'feature-variable', - 'test_user', - {}, - { - 'feature_key': 'test_feature_in_experiment', - 'feature_enabled': False, - 'source': 'rollout', - 'variable_key': 'environment', - 'variable_value': 'devel', - 'variable_type': 'string', - 'source_info': {} - } - ) - - def test_get_feature_variable__returns_none_if_none_feature_key(self): - """ Test that get_feature_variable_* returns None for None feature key. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - with mock.patch.object(opt_obj, 'logger') as mock_client_logger: - # Check for booleans - self.assertIsNone(opt_obj.get_feature_variable_boolean(None, 'variable_key', 'test_user')) - mock_client_logger.error.assert_called_with('Provided "feature_key" is in an invalid format.') - mock_client_logger.reset_mock() - - # Check for doubles - self.assertIsNone(opt_obj.get_feature_variable_double(None, 'variable_key', 'test_user')) - mock_client_logger.error.assert_called_with('Provided "feature_key" is in an invalid format.') - mock_client_logger.reset_mock() - - # Check for integers - self.assertIsNone(opt_obj.get_feature_variable_integer(None, 'variable_key', 'test_user')) - mock_client_logger.error.assert_called_with('Provided "feature_key" is in an invalid format.') - mock_client_logger.reset_mock() - - # Check for strings - self.assertIsNone(opt_obj.get_feature_variable_string(None, 'variable_key', 'test_user')) - mock_client_logger.error.assert_called_with('Provided "feature_key" is in an invalid format.') - mock_client_logger.reset_mock() - - # Check for non-typed - self.assertIsNone(opt_obj.get_feature_variable(None, 'variable_key', 'test_user')) - mock_client_logger.error.assert_called_with('Provided "feature_key" is in an invalid format.') - mock_client_logger.reset_mock() - - def test_get_feature_variable__returns_none_if_none_variable_key(self): - """ Test that get_feature_variable_* returns None for None variable key. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - with mock.patch.object(opt_obj, 'logger') as mock_client_logger: - # Check for booleans - self.assertIsNone(opt_obj.get_feature_variable_boolean('feature_key', None, 'test_user')) - mock_client_logger.error.assert_called_with('Provided "variable_key" is in an invalid format.') - mock_client_logger.reset_mock() - - # Check for doubles - self.assertIsNone(opt_obj.get_feature_variable_double('feature_key', None, 'test_user')) - mock_client_logger.error.assert_called_with('Provided "variable_key" is in an invalid format.') - mock_client_logger.reset_mock() - - # Check for integers - self.assertIsNone(opt_obj.get_feature_variable_integer('feature_key', None, 'test_user')) - mock_client_logger.error.assert_called_with('Provided "variable_key" is in an invalid format.') - mock_client_logger.reset_mock() - - # Check for strings - self.assertIsNone(opt_obj.get_feature_variable_string('feature_key', None, 'test-User')) - mock_client_logger.error.assert_called_with('Provided "variable_key" is in an invalid format.') - mock_client_logger.reset_mock() - - # Check for non-typed - self.assertIsNone(opt_obj.get_feature_variable('feature_key', None, 'test-User')) - mock_client_logger.error.assert_called_with('Provided "variable_key" is in an invalid format.') - mock_client_logger.reset_mock() - - def test_get_feature_variable__returns_none_if_none_user_id(self): - """ Test that get_feature_variable_* returns None for None user ID. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - with mock.patch.object(opt_obj, 'logger') as mock_client_logger: - # Check for booleans - self.assertIsNone(opt_obj.get_feature_variable_boolean('feature_key', 'variable_key', None)) - mock_client_logger.error.assert_called_with('Provided "user_id" is in an invalid format.') - mock_client_logger.reset_mock() - - # Check for doubles - self.assertIsNone(opt_obj.get_feature_variable_double('feature_key', 'variable_key', None)) - mock_client_logger.error.assert_called_with('Provided "user_id" is in an invalid format.') - mock_client_logger.reset_mock() - - # Check for integers - self.assertIsNone(opt_obj.get_feature_variable_integer('feature_key', 'variable_key', None)) - mock_client_logger.error.assert_called_with('Provided "user_id" is in an invalid format.') - mock_client_logger.reset_mock() - - # Check for strings - self.assertIsNone(opt_obj.get_feature_variable_string('feature_key', 'variable_key', None)) - mock_client_logger.error.assert_called_with('Provided "user_id" is in an invalid format.') - mock_client_logger.reset_mock() - - # Check for non-typed - self.assertIsNone(opt_obj.get_feature_variable('feature_key', 'variable_key', None)) - mock_client_logger.error.assert_called_with('Provided "user_id" is in an invalid format.') - mock_client_logger.reset_mock() - - def test_get_feature_variable__invalid_attributes(self): - """ Test that get_feature_variable_* returns None for invalid attributes. """ - - 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) as mock_validator: - - # get_feature_variable_boolean - self.assertIsNone( - opt_obj.get_feature_variable_boolean('test_feature_in_experiment', - 'is_working', '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.') - mock_validator.reset_mock() - mock_client_logging.reset_mock() - - # get_feature_variable_double - self.assertIsNone( - opt_obj.get_feature_variable_double('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.') - mock_validator.reset_mock() - mock_client_logging.reset_mock() - - # get_feature_variable_integer - self.assertIsNone( - opt_obj.get_feature_variable_integer('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.') - mock_validator.reset_mock() - mock_client_logging.reset_mock() - - # get_feature_variable_string - self.assertIsNone( - opt_obj.get_feature_variable_string('test_feature_in_experiment', - 'environment', '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.') - mock_validator.reset_mock() - mock_client_logging.reset_mock() - - # get_feature_variable - self.assertIsNone( - opt_obj.get_feature_variable('test_feature_in_experiment', - 'is_working', '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.') - mock_validator.reset_mock() - mock_client_logging.reset_mock() - - self.assertIsNone( - 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.') - mock_validator.reset_mock() - mock_client_logging.reset_mock() - - self.assertIsNone( - 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.') - mock_validator.reset_mock() - mock_client_logging.reset_mock() - - self.assertIsNone( - opt_obj.get_feature_variable('test_feature_in_experiment', - 'environment', '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.') - mock_validator.reset_mock() - mock_client_logging.reset_mock() - - def test_get_feature_variable__returns_none_if_invalid_feature_key(self): - """ Test that get_feature_variable_* returns None for invalid feature key. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - with mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logger: - self.assertIsNone(opt_obj.get_feature_variable_boolean('invalid_feature', 'is_working', 'test_user')) - self.assertIsNone(opt_obj.get_feature_variable_double('invalid_feature', 'cost', 'test_user')) - self.assertIsNone(opt_obj.get_feature_variable_integer('invalid_feature', 'count', 'test_user')) - self.assertIsNone(opt_obj.get_feature_variable_string('invalid_feature', 'environment', 'test_user')) - self.assertIsNone(opt_obj.get_feature_variable('invalid_feature', 'is_working', 'test_user')) - self.assertIsNone(opt_obj.get_feature_variable('invalid_feature', 'cost', 'test_user')) - self.assertIsNone(opt_obj.get_feature_variable('invalid_feature', 'count', 'test_user')) - self.assertIsNone(opt_obj.get_feature_variable('invalid_feature', 'environment', 'test_user')) - - self.assertEqual(8, mock_config_logger.error.call_count) - mock_config_logger.error.assert_has_calls([ - mock.call('Feature "invalid_feature" is not in datafile.'), - mock.call('Feature "invalid_feature" is not in datafile.'), - mock.call('Feature "invalid_feature" is not in datafile.'), - mock.call('Feature "invalid_feature" is not in datafile.'), - mock.call('Feature "invalid_feature" is not in datafile.'), - mock.call('Feature "invalid_feature" is not in datafile.'), - mock.call('Feature "invalid_feature" is not in datafile.'), - mock.call('Feature "invalid_feature" is not in datafile.') - ]) - - def test_get_feature_variable__returns_none_if_invalid_variable_key(self): - """ Test that get_feature_variable_* returns None for invalid variable key. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - with mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logger: - self.assertIsNone(opt_obj.get_feature_variable_boolean('test_feature_in_experiment', - 'invalid_variable', - 'test_user')) - self.assertIsNone(opt_obj.get_feature_variable_double('test_feature_in_experiment', - 'invalid_variable', - 'test_user')) - self.assertIsNone(opt_obj.get_feature_variable_integer('test_feature_in_experiment', - 'invalid_variable', - 'test_user')) - self.assertIsNone(opt_obj.get_feature_variable_string('test_feature_in_experiment', - 'invalid_variable', - 'test_user')) - self.assertIsNone(opt_obj.get_feature_variable('test_feature_in_experiment', - 'invalid_variable', - 'test_user')) - - self.assertEqual(5, mock_config_logger.error.call_count) - mock_config_logger.error.assert_has_calls([ - mock.call('Variable with key "invalid_variable" not found in the datafile.'), - mock.call('Variable with key "invalid_variable" not found in the datafile.'), - mock.call('Variable with key "invalid_variable" not found in the datafile.'), - mock.call('Variable with key "invalid_variable" not found in the datafile.'), - mock.call('Variable with key "invalid_variable" not found in the datafile.') - ]) - - def test_get_feature_variable__returns_default_value_if_feature_not_enabled(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)) - 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', '111128') - - # 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)), \ - 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')) - - mock_client_logger.info.assert_called_once_with( - 'Feature "test_feature_in_experiment" for variation "control" is not enabled. ' - 'Returning the default variable value "true".' - ) - - # 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)), \ - 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')) - - mock_client_logger.info.assert_called_once_with( - 'Feature "test_feature_in_experiment" for variation "control" is not enabled. ' - 'Returning the default variable value "10.99".' - ) - - # 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)), \ - 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')) - - mock_client_logger.info.assert_called_once_with( - 'Feature "test_feature_in_experiment" for variation "control" is not enabled. ' - 'Returning the default variable value "999".' - ) - - # 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)), \ - 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')) - - mock_client_logger.info.assert_called_once_with( - 'Feature "test_feature_in_experiment" for variation "control" is not enabled. ' - 'Returning the default variable value "devel".' - ) - - # 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)), \ - 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( - 'Feature "test_feature_in_experiment" for variation "control" is not enabled. ' - 'Returning the default variable value "true".' - ) - - with mock.patch('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')) - - mock_client_logger.info.assert_called_once_with( - 'Feature "test_feature_in_experiment" for variation "control" is not enabled. ' - 'Returning the default variable value "10.99".' - ) - - with mock.patch('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')) - - mock_client_logger.info.assert_called_once_with( - 'Feature "test_feature_in_experiment" for variation "control" is not enabled. ' - 'Returning the default variable value "999".' - ) - - with mock.patch('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')) - - mock_client_logger.info.assert_called_once_with( - 'Feature "test_feature_in_experiment" for variation "control" is not enabled. ' - 'Returning the default variable value "devel".' - ) - - 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)) - mock_experiment = opt_obj.config_manager.get_config().get_experiment_from_key('211127') - mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('211127', '211229') - - # Boolean - with mock.patch('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')) - - mock_client_logger.info.assert_called_once_with( - 'Feature "test_feature_in_rollout" for variation "211229" is not enabled. ' - 'Returning the default variable value "false".' - ) - - # Double - with mock.patch('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')) - - mock_client_logger.info.assert_called_once_with( - 'Feature "test_feature_in_rollout" for variation "211229" is not enabled. ' - 'Returning the default variable value "99.99".' - ) - - # Integer - with mock.patch('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')) - - mock_client_logger.info.assert_called_once_with( - 'Feature "test_feature_in_rollout" for variation "211229" is not enabled. ' - 'Returning the default variable value "999".' - ) - - # String - with mock.patch('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')) - mock_client_logger.info.assert_called_once_with( - 'Feature "test_feature_in_rollout" for variation "211229" is not enabled. ' - 'Returning the default variable value "Hello".' - ) - - # 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)), \ - 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')) - - mock_client_logger.info.assert_called_once_with( - 'Feature "test_feature_in_rollout" for variation "211229" is not enabled. ' - 'Returning the default variable value "false".' - ) - - with mock.patch('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')) - - mock_client_logger.info.assert_called_once_with( - 'Feature "test_feature_in_rollout" for variation "211229" is not enabled. ' - 'Returning the default variable value "99.99".' - ) - - with mock.patch('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')) - - mock_client_logger.info.assert_called_once_with( - 'Feature "test_feature_in_rollout" for variation "211229" is not enabled. ' - 'Returning the default variable value "999".' - ) - - with mock.patch('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')) - mock_client_logger.info.assert_called_once_with( - 'Feature "test_feature_in_rollout" for variation "211229" is not enabled. ' - 'Returning the default variable value "Hello".' - ) - - def test_get_feature_variable__returns_none_if_type_mismatch(self): - """ Test that get_feature_variable_* returns None if type mismatch. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - 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)), \ - 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(opt_obj.get_feature_variable_double('test_feature_in_experiment', 'is_working', 'test_user')) - - mock_client_logger.warning.assert_called_with( - 'Requested variable type "double", but variable is of type "boolean". ' - 'Use correct API to retrieve value. Returning None.' - ) - - def test_get_feature_variable__returns_none_if_unable_to_cast(self): - """ Test that get_feature_variable_* returns None if unable_to_cast_value """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) - 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)), \ - mock.patch('optimizely.project_config.ProjectConfig.get_typecast_value', - side_effect=ValueError()),\ - mock.patch.object(opt_obj, 'logger') as mock_client_logger: - self.assertEqual(None, opt_obj.get_feature_variable_integer('test_feature_in_experiment', 'count', 'test_user')) - self.assertEqual(None, opt_obj.get_feature_variable('test_feature_in_experiment', 'count', 'test_user')) - - mock_client_logger.error.assert_called_with('Unable to cast value. Returning None.') - - def test_get_feature_variable_returns__variable_value__typed_audience_match(self): - """ Test that get_feature_variable_* return variable value with typed audience match. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - - # Should be included in the feature test via greater-than match audience with id '3468206647' - with mock.patch.object(opt_obj, 'logger') as mock_client_logger: - self.assertEqual( - 'xyz', - opt_obj.get_feature_variable_string('feat_with_var', 'x', 'user1', {'lasers': 71}) - ) - mock_client_logger.info.assert_called_once_with( - 'Got variable value "xyz" for variable "x" of feature flag "feat_with_var".' - ) - - with mock.patch.object(opt_obj, 'logger') as mock_client_logger: - self.assertEqual( - 'xyz', - opt_obj.get_feature_variable('feat_with_var', 'x', 'user1', {'lasers': 71}) - ) - mock_client_logger.info.assert_called_once_with( - 'Got variable value "xyz" for variable "x" of feature flag "feat_with_var".' - ) - - # Should be included in the feature test via exact match boolean audience with id '3468206643' - with mock.patch.object(opt_obj, 'logger') as mock_client_logger: - self.assertEqual( - 'xyz', - opt_obj.get_feature_variable_string('feat_with_var', 'x', 'user1', {'should_do_it': True}) - ) - mock_client_logger.info.assert_called_once_with( - 'Got variable value "xyz" for variable "x" of feature flag "feat_with_var".' - ) - - with mock.patch.object(opt_obj, 'logger') as mock_client_logger: - self.assertEqual( - 'xyz', - opt_obj.get_feature_variable('feat_with_var', 'x', 'user1', {'should_do_it': True}) - ) - mock_client_logger.info.assert_called_once_with( - 'Got variable value "xyz" for variable "x" of feature flag "feat_with_var".' - ) - - """ Test that get_feature_variable_* return default value with typed audience mismatch. """ - def test_get_feature_variable_returns__default_value__typed_audience_match(self): - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - - self.assertEqual( - 'x', - opt_obj.get_feature_variable_string('feat_with_var', 'x', 'user1', {'lasers': 50}) - ) - self.assertEqual( - 'x', - opt_obj.get_feature_variable('feat_with_var', 'x', 'user1', {'lasers': 50}) - ) - - def test_get_feature_variable_returns__variable_value__complex_audience_match(self): - """ Test that get_feature_variable_* return variable value with complex audience match. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - - # Should be included via exact match string audience with id '3468206642', and - # greater than audience with id '3468206647' - user_attr = {'house': 'Gryffindor', 'lasers': 700} - self.assertEqual( - 150, - opt_obj.get_feature_variable_integer('feat2_with_var', 'z', 'user1', user_attr) - ) - self.assertEqual( - 150, - opt_obj.get_feature_variable('feat2_with_var', 'z', 'user1', user_attr) - ) - - def test_get_feature_variable_returns__default_value__complex_audience_match(self): - """ Test that get_feature_variable_* return default value with complex audience mismatch. """ - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) - - # Should be excluded - no audiences match with no attributes - self.assertEqual( - 10, - opt_obj.get_feature_variable_integer('feat2_with_var', 'z', 'user1', {}) - ) - self.assertEqual( - 10, - opt_obj.get_feature_variable('feat2_with_var', 'z', 'user1', {}) - ) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + + # Boolean + with mock.patch( + '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: + self.assertTrue( + opt_obj.get_feature_variable_boolean('test_feature_in_experiment', 'is_working', 'test_user') + ) + + mock_client_logger.info.assert_called_once_with( + 'User "test_user" is not in any variation or rollout rule. ' + 'Returning default value for variable "is_working" of feature flag "test_feature_in_experiment".' + ) + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': False, + 'source': 'rollout', + 'variable_key': 'is_working', + 'variable_value': True, + 'variable_type': 'boolean', + 'source_info': {}, + }, + ) -class OptimizelyWithExceptionTest(base.BaseTest): + mock_client_logger.info.reset_mock() + + # Double + with mock.patch( + '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: + self.assertEqual( + 10.99, opt_obj.get_feature_variable_double('test_feature_in_experiment', 'cost', 'test_user'), + ) + + mock_client_logger.info.assert_called_once_with( + 'User "test_user" is not in any variation or rollout rule. ' + 'Returning default value for variable "cost" of feature flag "test_feature_in_experiment".' + ) - def setUp(self): - base.BaseTest.setUp(self) - self.optimizely = optimizely.Optimizely(json.dumps(self.config_dict), - error_handler=error_handler.RaiseExceptionErrorHandler) + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': False, + 'source': 'rollout', + 'variable_key': 'cost', + 'variable_value': 10.99, + 'variable_type': 'double', + 'source_info': {}, + }, + ) - def test_activate__with_attributes__invalid_attributes(self): - """ Test that activate raises exception if attributes are in invalid format. """ + mock_client_logger.info.reset_mock() + + # Integer + with mock.patch( + '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: + self.assertEqual( + 999, opt_obj.get_feature_variable_integer('test_feature_in_experiment', 'count', 'test_user'), + ) + + mock_client_logger.info.assert_called_once_with( + 'User "test_user" is not in any variation or rollout rule. ' + 'Returning default value for variable "count" of feature flag "test_feature_in_experiment".' + ) - self.assertRaisesRegexp(exceptions.InvalidAttributeException, enums.Errors.INVALID_ATTRIBUTE_FORMAT, - self.optimizely.activate, 'test_experiment', 'test_user', attributes='invalid') + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': False, + 'source': 'rollout', + 'variable_key': 'count', + 'variable_value': 999, + 'variable_type': 'integer', + 'source_info': {}, + }, + ) - def test_track__with_attributes__invalid_attributes(self): - """ Test that track raises exception if attributes are in invalid format. """ + mock_client_logger.info.reset_mock() + + # String + with mock.patch( + '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: + self.assertEqual( + 'devel', opt_obj.get_feature_variable_string('test_feature_in_experiment', 'environment', 'test_user'), + ) + + mock_client_logger.info.assert_called_once_with( + 'User "test_user" is not in any variation or rollout rule. ' + 'Returning default value for variable "environment" of feature flag "test_feature_in_experiment".' + ) - self.assertRaisesRegexp(exceptions.InvalidAttributeException, enums.Errors.INVALID_ATTRIBUTE_FORMAT, - self.optimizely.track, 'test_event', 'test_user', attributes='invalid') + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': False, + 'source': 'rollout', + 'variable_key': 'environment', + 'variable_value': 'devel', + 'variable_type': 'string', + 'source_info': {}, + }, + ) - def test_track__with_event_tag__invalid_event_tag(self): - """ Test that track raises exception if event_tag is in invalid format. """ + mock_client_logger.info.reset_mock() - self.assertRaisesRegexp(exceptions.InvalidEventTagException, enums.Errors.INVALID_EVENT_TAG_FORMAT, - self.optimizely.track, 'test_event', 'test_user', event_tags=4200) + # Non-typed + with mock.patch( + '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: + self.assertTrue(opt_obj.get_feature_variable('test_feature_in_experiment', 'is_working', 'test_user')) - def test_get_variation__with_attributes__invalid_attributes(self): - """ Test that get variation raises exception if attributes are in invalid format. """ + mock_client_logger.info.assert_called_once_with( + 'User "test_user" is not in any variation or rollout rule. ' + 'Returning default value for variable "is_working" of feature flag "test_feature_in_experiment".' + ) - self.assertRaisesRegexp(exceptions.InvalidAttributeException, enums.Errors.INVALID_ATTRIBUTE_FORMAT, - self.optimizely.get_variation, 'test_experiment', 'test_user', attributes='invalid') + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': False, + 'source': 'rollout', + 'variable_key': 'is_working', + 'variable_value': True, + 'variable_type': 'boolean', + 'source_info': {}, + }, + ) + + 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), + ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision: + self.assertEqual( + 10.99, opt_obj.get_feature_variable('test_feature_in_experiment', 'cost', 'test_user'), + ) + + mock_client_logger.info.assert_called_once_with( + 'User "test_user" is not in any variation or rollout rule. ' + 'Returning default value for variable "cost" of feature flag "test_feature_in_experiment".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': False, + 'source': 'rollout', + 'variable_key': 'cost', + 'variable_value': 10.99, + 'variable_type': 'double', + 'source_info': {}, + }, + ) + + 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), + ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision: + self.assertEqual( + 999, opt_obj.get_feature_variable('test_feature_in_experiment', 'count', 'test_user'), + ) + + mock_client_logger.info.assert_called_once_with( + 'User "test_user" is not in any variation or rollout rule. ' + 'Returning default value for variable "count" of feature flag "test_feature_in_experiment".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': False, + 'source': 'rollout', + 'variable_key': 'count', + 'variable_value': 999, + 'variable_type': 'integer', + 'source_info': {}, + }, + ) + + 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), + ), mock.patch.object(opt_obj, 'logger') as mock_client_logger, mock.patch( + 'optimizely.notification_center.NotificationCenter.send_notifications' + ) as mock_broadcast_decision: + self.assertEqual( + 'devel', opt_obj.get_feature_variable('test_feature_in_experiment', 'environment', 'test_user'), + ) + + mock_client_logger.info.assert_called_once_with( + 'User "test_user" is not in any variation or rollout rule. ' + 'Returning default value for variable "environment" of feature flag "test_feature_in_experiment".' + ) + + mock_broadcast_decision.assert_called_once_with( + enums.NotificationTypes.DECISION, + 'feature-variable', + 'test_user', + {}, + { + 'feature_key': 'test_feature_in_experiment', + 'feature_enabled': False, + 'source': 'rollout', + 'variable_key': 'environment', + 'variable_value': 'devel', + 'variable_type': 'string', + 'source_info': {}, + }, + ) + + def test_get_feature_variable__returns_none_if_none_feature_key(self): + """ Test that get_feature_variable_* returns None for None feature key. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + with mock.patch.object(opt_obj, 'logger') as mock_client_logger: + # Check for booleans + self.assertIsNone(opt_obj.get_feature_variable_boolean(None, 'variable_key', 'test_user')) + mock_client_logger.error.assert_called_with('Provided "feature_key" is in an invalid format.') + mock_client_logger.reset_mock() + + # Check for doubles + self.assertIsNone(opt_obj.get_feature_variable_double(None, 'variable_key', 'test_user')) + mock_client_logger.error.assert_called_with('Provided "feature_key" is in an invalid format.') + mock_client_logger.reset_mock() + + # Check for integers + self.assertIsNone(opt_obj.get_feature_variable_integer(None, 'variable_key', 'test_user')) + mock_client_logger.error.assert_called_with('Provided "feature_key" is in an invalid format.') + mock_client_logger.reset_mock() + + # Check for strings + self.assertIsNone(opt_obj.get_feature_variable_string(None, 'variable_key', 'test_user')) + mock_client_logger.error.assert_called_with('Provided "feature_key" is in an invalid format.') + mock_client_logger.reset_mock() + + # Check for non-typed + self.assertIsNone(opt_obj.get_feature_variable(None, 'variable_key', 'test_user')) + mock_client_logger.error.assert_called_with('Provided "feature_key" is in an invalid format.') + mock_client_logger.reset_mock() + + def test_get_feature_variable__returns_none_if_none_variable_key(self): + """ Test that get_feature_variable_* returns None for None variable key. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + with mock.patch.object(opt_obj, 'logger') as mock_client_logger: + # Check for booleans + self.assertIsNone(opt_obj.get_feature_variable_boolean('feature_key', None, 'test_user')) + mock_client_logger.error.assert_called_with('Provided "variable_key" is in an invalid format.') + mock_client_logger.reset_mock() + + # Check for doubles + self.assertIsNone(opt_obj.get_feature_variable_double('feature_key', None, 'test_user')) + mock_client_logger.error.assert_called_with('Provided "variable_key" is in an invalid format.') + mock_client_logger.reset_mock() + + # Check for integers + self.assertIsNone(opt_obj.get_feature_variable_integer('feature_key', None, 'test_user')) + mock_client_logger.error.assert_called_with('Provided "variable_key" is in an invalid format.') + mock_client_logger.reset_mock() + + # Check for strings + self.assertIsNone(opt_obj.get_feature_variable_string('feature_key', None, 'test-User')) + mock_client_logger.error.assert_called_with('Provided "variable_key" is in an invalid format.') + mock_client_logger.reset_mock() + + # Check for non-typed + self.assertIsNone(opt_obj.get_feature_variable('feature_key', None, 'test-User')) + mock_client_logger.error.assert_called_with('Provided "variable_key" is in an invalid format.') + mock_client_logger.reset_mock() + + def test_get_feature_variable__returns_none_if_none_user_id(self): + """ Test that get_feature_variable_* returns None for None user ID. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + with mock.patch.object(opt_obj, 'logger') as mock_client_logger: + # Check for booleans + self.assertIsNone(opt_obj.get_feature_variable_boolean('feature_key', 'variable_key', None)) + mock_client_logger.error.assert_called_with('Provided "user_id" is in an invalid format.') + mock_client_logger.reset_mock() + + # Check for doubles + self.assertIsNone(opt_obj.get_feature_variable_double('feature_key', 'variable_key', None)) + mock_client_logger.error.assert_called_with('Provided "user_id" is in an invalid format.') + mock_client_logger.reset_mock() + + # Check for integers + self.assertIsNone(opt_obj.get_feature_variable_integer('feature_key', 'variable_key', None)) + mock_client_logger.error.assert_called_with('Provided "user_id" is in an invalid format.') + mock_client_logger.reset_mock() + + # Check for strings + self.assertIsNone(opt_obj.get_feature_variable_string('feature_key', 'variable_key', None)) + mock_client_logger.error.assert_called_with('Provided "user_id" is in an invalid format.') + mock_client_logger.reset_mock() + + # Check for non-typed + self.assertIsNone(opt_obj.get_feature_variable('feature_key', 'variable_key', None)) + mock_client_logger.error.assert_called_with('Provided "user_id" is in an invalid format.') + mock_client_logger.reset_mock() + + def test_get_feature_variable__invalid_attributes(self): + """ Test that get_feature_variable_* returns None for invalid attributes. """ + + 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 + ) as mock_validator: + + # get_feature_variable_boolean + self.assertIsNone( + opt_obj.get_feature_variable_boolean( + 'test_feature_in_experiment', 'is_working', '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.') + mock_validator.reset_mock() + mock_client_logging.reset_mock() + + # get_feature_variable_double + self.assertIsNone( + opt_obj.get_feature_variable_double( + '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.') + mock_validator.reset_mock() + mock_client_logging.reset_mock() + + # get_feature_variable_integer + self.assertIsNone( + opt_obj.get_feature_variable_integer( + '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.') + mock_validator.reset_mock() + mock_client_logging.reset_mock() + + # get_feature_variable_string + self.assertIsNone( + opt_obj.get_feature_variable_string( + 'test_feature_in_experiment', 'environment', '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.') + mock_validator.reset_mock() + mock_client_logging.reset_mock() + + # get_feature_variable + self.assertIsNone( + opt_obj.get_feature_variable( + 'test_feature_in_experiment', 'is_working', '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.') + mock_validator.reset_mock() + mock_client_logging.reset_mock() + + self.assertIsNone( + 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.') + mock_validator.reset_mock() + mock_client_logging.reset_mock() + + self.assertIsNone( + 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.') + mock_validator.reset_mock() + mock_client_logging.reset_mock() + + self.assertIsNone( + opt_obj.get_feature_variable( + 'test_feature_in_experiment', 'environment', '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.') + mock_validator.reset_mock() + mock_client_logging.reset_mock() + + def test_get_feature_variable__returns_none_if_invalid_feature_key(self): + """ Test that get_feature_variable_* returns None for invalid feature key. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + with mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logger: + self.assertIsNone(opt_obj.get_feature_variable_boolean('invalid_feature', 'is_working', 'test_user')) + self.assertIsNone(opt_obj.get_feature_variable_double('invalid_feature', 'cost', 'test_user')) + self.assertIsNone(opt_obj.get_feature_variable_integer('invalid_feature', 'count', 'test_user')) + self.assertIsNone(opt_obj.get_feature_variable_string('invalid_feature', 'environment', 'test_user')) + self.assertIsNone(opt_obj.get_feature_variable('invalid_feature', 'is_working', 'test_user')) + self.assertIsNone(opt_obj.get_feature_variable('invalid_feature', 'cost', 'test_user')) + self.assertIsNone(opt_obj.get_feature_variable('invalid_feature', 'count', 'test_user')) + self.assertIsNone(opt_obj.get_feature_variable('invalid_feature', 'environment', 'test_user')) + + self.assertEqual(8, mock_config_logger.error.call_count) + mock_config_logger.error.assert_has_calls( + [ + mock.call('Feature "invalid_feature" is not in datafile.'), + mock.call('Feature "invalid_feature" is not in datafile.'), + mock.call('Feature "invalid_feature" is not in datafile.'), + mock.call('Feature "invalid_feature" is not in datafile.'), + mock.call('Feature "invalid_feature" is not in datafile.'), + mock.call('Feature "invalid_feature" is not in datafile.'), + mock.call('Feature "invalid_feature" is not in datafile.'), + mock.call('Feature "invalid_feature" is not in datafile.'), + ] + ) + + def test_get_feature_variable__returns_none_if_invalid_variable_key(self): + """ Test that get_feature_variable_* returns None for invalid variable key. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + with mock.patch.object(opt_obj.config_manager.get_config(), 'logger') as mock_config_logger: + self.assertIsNone( + opt_obj.get_feature_variable_boolean('test_feature_in_experiment', 'invalid_variable', 'test_user') + ) + self.assertIsNone( + opt_obj.get_feature_variable_double('test_feature_in_experiment', 'invalid_variable', 'test_user') + ) + self.assertIsNone( + opt_obj.get_feature_variable_integer('test_feature_in_experiment', 'invalid_variable', 'test_user') + ) + self.assertIsNone( + opt_obj.get_feature_variable_string('test_feature_in_experiment', 'invalid_variable', 'test_user') + ) + self.assertIsNone( + opt_obj.get_feature_variable('test_feature_in_experiment', 'invalid_variable', 'test_user') + ) + + self.assertEqual(5, mock_config_logger.error.call_count) + mock_config_logger.error.assert_has_calls( + [ + mock.call('Variable with key "invalid_variable" not found in the datafile.'), + mock.call('Variable with key "invalid_variable" not found in the datafile.'), + mock.call('Variable with key "invalid_variable" not found in the datafile.'), + mock.call('Variable with key "invalid_variable" not found in the datafile.'), + mock.call('Variable with key "invalid_variable" not found in the datafile.'), + ] + ) + + def test_get_feature_variable__returns_default_value_if_feature_not_enabled(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)) + 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', '111128') + + # 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), + ), 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') + ) + + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_experiment" for variation "control" is not enabled. ' + 'Returning the default variable value "true".' + ) + + # 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), + ), 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'), + ) + + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_experiment" for variation "control" is not enabled. ' + 'Returning the default variable value "10.99".' + ) + + # 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), + ), 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'), + ) + + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_experiment" for variation "control" is not enabled. ' + 'Returning the default variable value "999".' + ) + + # 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), + ), 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'), + ) + + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_experiment" for variation "control" is not enabled. ' + 'Returning the default variable value "devel".' + ) + + # 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), + ), 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( + 'Feature "test_feature_in_experiment" for variation "control" is not enabled. ' + 'Returning the default variable value "true".' + ) + + with mock.patch( + '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'), + ) + + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_experiment" for variation "control" is not enabled. ' + 'Returning the default variable value "10.99".' + ) + + with mock.patch( + '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'), + ) + + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_experiment" for variation "control" is not enabled. ' + 'Returning the default variable value "999".' + ) + + with mock.patch( + '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'), + ) + + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_experiment" for variation "control" is not enabled. ' + 'Returning the default variable value "devel".' + ) + + 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)) + mock_experiment = opt_obj.config_manager.get_config().get_experiment_from_key('211127') + mock_variation = opt_obj.config_manager.get_config().get_variation_from_id('211127', '211229') + + # Boolean + with mock.patch( + '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')) + + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_rollout" for variation "211229" is not enabled. ' + 'Returning the default variable value "false".' + ) + + # Double + with mock.patch( + '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'), + ) + + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_rollout" for variation "211229" is not enabled. ' + 'Returning the default variable value "99.99".' + ) + + # Integer + with mock.patch( + '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'), + ) + + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_rollout" for variation "211229" is not enabled. ' + 'Returning the default variable value "999".' + ) + + # String + with mock.patch( + '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'), + ) + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_rollout" for variation "211229" is not enabled. ' + 'Returning the default variable value "Hello".' + ) + + # 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), + ), 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')) + + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_rollout" for variation "211229" is not enabled. ' + 'Returning the default variable value "false".' + ) + + with mock.patch( + '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'), + ) + + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_rollout" for variation "211229" is not enabled. ' + 'Returning the default variable value "99.99".' + ) + + with mock.patch( + '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'), + ) + + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_rollout" for variation "211229" is not enabled. ' + 'Returning the default variable value "999".' + ) + + with mock.patch( + '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'), + ) + mock_client_logger.info.assert_called_once_with( + 'Feature "test_feature_in_rollout" for variation "211229" is not enabled. ' + 'Returning the default variable value "Hello".' + ) + + def test_get_feature_variable__returns_none_if_type_mismatch(self): + """ Test that get_feature_variable_* returns None if type mismatch. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + 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), + ), 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( + opt_obj.get_feature_variable_double('test_feature_in_experiment', 'is_working', 'test_user') + ) + + mock_client_logger.warning.assert_called_with( + 'Requested variable type "double", but variable is of type "boolean". ' + 'Use correct API to retrieve value. Returning None.' + ) + + def test_get_feature_variable__returns_none_if_unable_to_cast(self): + """ Test that get_feature_variable_* returns None if unable_to_cast_value """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_features)) + 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), + ), mock.patch( + 'optimizely.project_config.ProjectConfig.get_typecast_value', side_effect=ValueError(), + ), mock.patch.object( + opt_obj, 'logger' + ) as mock_client_logger: + self.assertEqual( + None, opt_obj.get_feature_variable_integer('test_feature_in_experiment', 'count', 'test_user'), + ) + self.assertEqual( + None, opt_obj.get_feature_variable('test_feature_in_experiment', 'count', 'test_user'), + ) + + mock_client_logger.error.assert_called_with('Unable to cast value. Returning None.') + + def test_get_feature_variable_returns__variable_value__typed_audience_match(self): + """ Test that get_feature_variable_* return variable value with typed audience match. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) + + # Should be included in the feature test via greater-than match audience with id '3468206647' + with mock.patch.object(opt_obj, 'logger') as mock_client_logger: + self.assertEqual( + 'xyz', opt_obj.get_feature_variable_string('feat_with_var', 'x', 'user1', {'lasers': 71}), + ) + mock_client_logger.info.assert_called_once_with( + 'Got variable value "xyz" for variable "x" of feature flag "feat_with_var".' + ) + + with mock.patch.object(opt_obj, 'logger') as mock_client_logger: + self.assertEqual( + 'xyz', opt_obj.get_feature_variable('feat_with_var', 'x', 'user1', {'lasers': 71}), + ) + mock_client_logger.info.assert_called_once_with( + 'Got variable value "xyz" for variable "x" of feature flag "feat_with_var".' + ) + + # Should be included in the feature test via exact match boolean audience with id '3468206643' + with mock.patch.object(opt_obj, 'logger') as mock_client_logger: + self.assertEqual( + 'xyz', opt_obj.get_feature_variable_string('feat_with_var', 'x', 'user1', {'should_do_it': True}), + ) + mock_client_logger.info.assert_called_once_with( + 'Got variable value "xyz" for variable "x" of feature flag "feat_with_var".' + ) + + with mock.patch.object(opt_obj, 'logger') as mock_client_logger: + self.assertEqual( + 'xyz', opt_obj.get_feature_variable('feat_with_var', 'x', 'user1', {'should_do_it': True}), + ) + mock_client_logger.info.assert_called_once_with( + 'Got variable value "xyz" for variable "x" of feature flag "feat_with_var".' + ) + + """ Test that get_feature_variable_* return default value with typed audience mismatch. """ + + def test_get_feature_variable_returns__default_value__typed_audience_match(self): + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) + + self.assertEqual( + 'x', opt_obj.get_feature_variable_string('feat_with_var', 'x', 'user1', {'lasers': 50}), + ) + self.assertEqual( + 'x', opt_obj.get_feature_variable('feat_with_var', 'x', 'user1', {'lasers': 50}), + ) + + def test_get_feature_variable_returns__variable_value__complex_audience_match(self): + """ Test that get_feature_variable_* return variable value with complex audience match. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) + + # Should be included via exact match string audience with id '3468206642', and + # greater than audience with id '3468206647' + user_attr = {'house': 'Gryffindor', 'lasers': 700} + self.assertEqual( + 150, opt_obj.get_feature_variable_integer('feat2_with_var', 'z', 'user1', user_attr), + ) + self.assertEqual(150, opt_obj.get_feature_variable('feat2_with_var', 'z', 'user1', user_attr)) + + def test_get_feature_variable_returns__default_value__complex_audience_match(self): + """ Test that get_feature_variable_* return default value with complex audience mismatch. """ + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict_with_typed_audiences)) + + # Should be excluded - no audiences match with no attributes + self.assertEqual(10, opt_obj.get_feature_variable_integer('feat2_with_var', 'z', 'user1', {})) + self.assertEqual(10, opt_obj.get_feature_variable('feat2_with_var', 'z', 'user1', {})) + + +class OptimizelyWithExceptionTest(base.BaseTest): + def setUp(self): + base.BaseTest.setUp(self) + self.optimizely = optimizely.Optimizely( + json.dumps(self.config_dict), error_handler=error_handler.RaiseExceptionErrorHandler, + ) + + def test_activate__with_attributes__invalid_attributes(self): + """ Test that activate raises exception if attributes are in invalid format. """ + + self.assertRaisesRegexp( + exceptions.InvalidAttributeException, + enums.Errors.INVALID_ATTRIBUTE_FORMAT, + self.optimizely.activate, + 'test_experiment', + 'test_user', + attributes='invalid', + ) + + def test_track__with_attributes__invalid_attributes(self): + """ Test that track raises exception if attributes are in invalid format. """ + + self.assertRaisesRegexp( + exceptions.InvalidAttributeException, + enums.Errors.INVALID_ATTRIBUTE_FORMAT, + self.optimizely.track, + 'test_event', + 'test_user', + attributes='invalid', + ) + + def test_track__with_event_tag__invalid_event_tag(self): + """ Test that track raises exception if event_tag is in invalid format. """ + + self.assertRaisesRegexp( + exceptions.InvalidEventTagException, + enums.Errors.INVALID_EVENT_TAG_FORMAT, + self.optimizely.track, + 'test_event', + 'test_user', + event_tags=4200, + ) + + def test_get_variation__with_attributes__invalid_attributes(self): + """ Test that get variation raises exception if attributes are in invalid format. """ + + self.assertRaisesRegexp( + exceptions.InvalidAttributeException, + enums.Errors.INVALID_ATTRIBUTE_FORMAT, + self.optimizely.get_variation, + 'test_experiment', + 'test_user', + attributes='invalid', + ) class OptimizelyWithLoggingTest(base.BaseTest): + def setUp(self): + base.BaseTest.setUp(self) + self.optimizely = optimizely.Optimizely(json.dumps(self.config_dict), logger=logger.SimpleLogger()) + self.project_config = self.optimizely.config_manager.get_config() + + def test_activate(self): + """ Test that expected log messages are logged during activate. """ + + variation_key = 'variation' + experiment_key = 'test_experiment' + 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'), + ), mock.patch('time.time', return_value=42), mock.patch( + 'optimizely.event.event_processor.ForwardingEventProcessor.process' + ), mock.patch.object( + self.optimizely, 'logger' + ) as mock_client_logging: + self.assertEqual(variation_key, self.optimizely.activate(experiment_key, user_id)) + + mock_client_logging.info.assert_called_once_with('Activating user "test_user" in experiment "test_experiment".') + + def test_track(self): + """ Test that expected log messages are logged during track. """ + + user_id = 'test_user' + event_key = 'test_event' + mock_client_logger = mock.patch.object(self.optimizely, 'logger') + + event_builder.Event('logx.optimizely.com', {'event_key': event_key}) + with mock.patch( + 'optimizely.event.event_processor.ForwardingEventProcessor.process' + ), mock_client_logger as mock_client_logging: + self.optimizely.track(event_key, user_id) + + mock_client_logging.info.assert_has_calls( + [mock.call('Tracking event "%s" for user "%s".' % (event_key, user_id))] + ) + + def test_activate__experiment_not_running(self): + """ Test that expected log messages are logged during activate when experiment is not running. """ + + 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 + ) as mock_is_experiment_running: + self.optimizely.activate( + 'test_experiment', 'test_user', attributes={'test_attribute': 'test_value'}, + ) + + mock_decision_logging.info.assert_called_once_with('Experiment "test_experiment" is not running.') + mock_client_logging.info.assert_called_once_with('Not activating user "test_user".') + mock_is_experiment_running.assert_called_once_with( + self.project_config.get_experiment_from_key('test_experiment') + ) + + def test_activate__no_audience_match(self): + """ Test that expected log messages are logged during activate when audience conditions are not met. """ + + mock_client_logger = mock.patch.object(self.optimizely, 'logger') + mock_decision_logger = mock.patch.object(self.optimizely.decision_service, 'logger') + + with mock_decision_logger as mock_decision_logging, mock_client_logger as mock_client_logging: + self.optimizely.activate( + 'test_experiment', 'test_user', attributes={'test_attribute': 'wrong_test_value'}, + ) - def setUp(self): - base.BaseTest.setUp(self) - self.optimizely = optimizely.Optimizely( - json.dumps(self.config_dict), - logger=logger.SimpleLogger() - ) - self.project_config = self.optimizely.config_manager.get_config() - - def test_activate(self): - """ Test that expected log messages are logged during activate. """ - - variation_key = 'variation' - experiment_key = 'test_experiment' - 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')), \ - mock.patch('time.time', return_value=42), \ - mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process'), \ - mock.patch.object(self.optimizely, 'logger') as mock_client_logging: - self.assertEqual(variation_key, self.optimizely.activate(experiment_key, user_id)) - - mock_client_logging.info.assert_called_once_with( - 'Activating user "test_user" in experiment "test_experiment".' - ) - - def test_track(self): - """ Test that expected log messages are logged during track. """ - - user_id = 'test_user' - event_key = 'test_event' - mock_client_logger = mock.patch.object(self.optimizely, 'logger') - - event_builder.Event('logx.optimizely.com', {'event_key': event_key}) - with mock.patch('optimizely.event.event_processor.ForwardingEventProcessor.process'), \ - mock_client_logger as mock_client_logging: - self.optimizely.track(event_key, user_id) - - mock_client_logging.info.assert_has_calls([ - mock.call('Tracking event "%s" for user "%s".' % (event_key, user_id)), - ]) - - def test_activate__experiment_not_running(self): - """ Test that expected log messages are logged during activate when experiment is not running. """ - - 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) as mock_is_experiment_running: - self.optimizely.activate('test_experiment', 'test_user', attributes={'test_attribute': 'test_value'}) - - mock_decision_logging.info.assert_called_once_with('Experiment "test_experiment" is not running.') - mock_client_logging.info.assert_called_once_with('Not activating user "test_user".') - mock_is_experiment_running.assert_called_once_with(self.project_config.get_experiment_from_key('test_experiment')) - - def test_activate__no_audience_match(self): - """ Test that expected log messages are logged during activate when audience conditions are not met. """ - - mock_client_logger = mock.patch.object(self.optimizely, 'logger') - mock_decision_logger = mock.patch.object(self.optimizely.decision_service, 'logger') - - with mock_decision_logger as mock_decision_logging, \ - mock_client_logger as mock_client_logging: - self.optimizely.activate( - 'test_experiment', - 'test_user', - attributes={'test_attribute': 'wrong_test_value'} - ) - - mock_decision_logging.debug.assert_any_call( - 'User "test_user" is not in the forced variation map.' - ) - mock_decision_logging.info.assert_called_with( - 'User "test_user" does not meet conditions to be in experiment "test_experiment".' - ) - mock_client_logging.info.assert_called_once_with('Not activating user "test_user".') - - def test_track__invalid_attributes(self): - """ Test that expected log messages are logged during track when attributes are in invalid format. """ - - mock_logger = mock.patch.object(self.optimizely, 'logger') - with mock_logger as mock_logging: - self.optimizely.track('test_event', 'test_user', attributes='invalid') - - mock_logging.error.assert_called_once_with('Provided attributes are in an invalid format.') - - def test_track__invalid_event_tag(self): - """ Test that expected log messages are logged during track when event_tag is in invalid format. """ - - mock_client_logger = mock.patch.object(self.optimizely, 'logger') - with mock_client_logger as mock_client_logging: - self.optimizely.track('test_event', 'test_user', event_tags='4200') - mock_client_logging.error.assert_called_once_with( - 'Provided event tags are in an invalid format.' - ) - - with mock_client_logger as mock_client_logging: - self.optimizely.track('test_event', 'test_user', event_tags=4200) - mock_client_logging.error.assert_called_once_with( - 'Provided event tags are in an invalid format.' - ) - - def test_get_variation__invalid_attributes(self): - """ Test that expected log messages are logged during get variation when attributes are in invalid format. """ - with mock.patch.object(self.optimizely, 'logger') as mock_client_logging: - self.optimizely.get_variation('test_experiment', 'test_user', attributes='invalid') - - mock_client_logging.error.assert_called_once_with('Provided attributes are in an invalid format.') - - def test_get_variation__invalid_experiment_key(self): - """ Test that None is returned and expected log messages are logged during get_variation \ + mock_decision_logging.debug.assert_any_call('User "test_user" is not in the forced variation map.') + mock_decision_logging.info.assert_called_with( + 'User "test_user" does not meet conditions to be in experiment "test_experiment".' + ) + mock_client_logging.info.assert_called_once_with('Not activating user "test_user".') + + def test_track__invalid_attributes(self): + """ Test that expected log messages are logged during track when attributes are in invalid format. """ + + mock_logger = mock.patch.object(self.optimizely, 'logger') + with mock_logger as mock_logging: + self.optimizely.track('test_event', 'test_user', attributes='invalid') + + mock_logging.error.assert_called_once_with('Provided attributes are in an invalid format.') + + def test_track__invalid_event_tag(self): + """ Test that expected log messages are logged during track when event_tag is in invalid format. """ + + mock_client_logger = mock.patch.object(self.optimizely, 'logger') + with mock_client_logger as mock_client_logging: + self.optimizely.track('test_event', 'test_user', event_tags='4200') + mock_client_logging.error.assert_called_once_with('Provided event tags are in an invalid format.') + + with mock_client_logger as mock_client_logging: + self.optimizely.track('test_event', 'test_user', event_tags=4200) + mock_client_logging.error.assert_called_once_with('Provided event tags are in an invalid format.') + + def test_get_variation__invalid_attributes(self): + """ Test that expected log messages are logged during get variation when attributes are in invalid format. """ + with mock.patch.object(self.optimizely, 'logger') as mock_client_logging: + self.optimizely.get_variation('test_experiment', 'test_user', attributes='invalid') + + mock_client_logging.error.assert_called_once_with('Provided attributes are in an invalid format.') + + def test_get_variation__invalid_experiment_key(self): + """ Test that None is returned and expected log messages are logged during get_variation \ 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) as mock_validator: - self.assertIsNone(self.optimizely.get_variation(99, 'test_user')) + with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, mock.patch( + 'optimizely.helpers.validator.is_non_empty_string', return_value=False + ) as mock_validator: + self.assertIsNone(self.optimizely.get_variation(99, 'test_user')) - mock_validator.assert_any_call(99) - mock_client_logging.error.assert_called_once_with('Provided "experiment_key" is in an invalid format.') + mock_validator.assert_any_call(99) + mock_client_logging.error.assert_called_once_with('Provided "experiment_key" is in an invalid format.') - def test_get_variation__invalid_user_id(self): - """ Test that None is returned and expected log messages are logged during get_variation \ + def test_get_variation__invalid_user_id(self): + """ Test that None is returned and expected log messages are logged during get_variation \ when user_id is in invalid format. """ - with mock.patch.object(self.optimizely, 'logger') as mock_client_logging: - self.assertIsNone(self.optimizely.get_variation('test_experiment', 99)) - mock_client_logging.error.assert_called_once_with('Provided "user_id" is in an invalid format.') + with mock.patch.object(self.optimizely, 'logger') as mock_client_logging: + self.assertIsNone(self.optimizely.get_variation('test_experiment', 99)) + mock_client_logging.error.assert_called_once_with('Provided "user_id" is in an invalid format.') - def test_activate__invalid_experiment_key(self): - """ Test that None is returned and expected log messages are logged during activate \ + def test_activate__invalid_experiment_key(self): + """ Test that None is returned and expected log messages are logged during activate \ 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) as mock_validator: - self.assertIsNone(self.optimizely.activate(99, 'test_user')) + with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, mock.patch( + 'optimizely.helpers.validator.is_non_empty_string', return_value=False + ) as mock_validator: + self.assertIsNone(self.optimizely.activate(99, 'test_user')) - mock_validator.assert_any_call(99) + mock_validator.assert_any_call(99) - mock_client_logging.error.assert_called_once_with('Provided "experiment_key" is in an invalid format.') + mock_client_logging.error.assert_called_once_with('Provided "experiment_key" is in an invalid format.') - def test_activate__invalid_user_id(self): - """ Test that None is returned and expected log messages are logged during activate \ + def test_activate__invalid_user_id(self): + """ Test that None is returned and expected log messages are logged during activate \ when user_id is in invalid format. """ - with mock.patch.object(self.optimizely, 'logger') as mock_client_logging: - self.assertIsNone(self.optimizely.activate('test_experiment', 99)) - - mock_client_logging.error.assert_called_once_with('Provided "user_id" is in an invalid format.') - - def test_activate__empty_user_id(self): - """ Test that expected log messages are logged during activate. """ - - variation_key = 'variation' - experiment_key = 'test_experiment' - user_id = '' - - with mock.patch('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(self.optimizely, 'logger') as mock_client_logging: - self.assertEqual(variation_key, self.optimizely.activate(experiment_key, user_id)) - - mock_client_logging.info.assert_called_once_with( - 'Activating user "" in experiment "test_experiment".' - ) - - def test_activate__invalid_attributes(self): - """ Test that expected log messages are logged during activate when attributes are in invalid format. """ - with mock.patch.object(self.optimizely, 'logger') as mock_client_logging: - self.optimizely.activate('test_experiment', 'test_user', attributes='invalid') - - mock_client_logging.error.assert_called_once_with('Provided attributes are in an invalid format.') - mock_client_logging.info.assert_called_once_with('Not activating user "test_user".') - - 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) as mock_is_experiment_running: - self.optimizely.get_variation('test_experiment', 'test_user', attributes={'test_attribute': 'test_value'}) - - mock_decision_logging.info.assert_called_once_with('Experiment "test_experiment" is not running.') - mock_is_experiment_running.assert_called_once_with(self.project_config.get_experiment_from_key('test_experiment')) - - def test_get_variation__no_audience_match(self): - """ Test that expected log messages are logged during get variation when audience conditions are not met. """ - - experiment_key = 'test_experiment' - user_id = 'test_user' - - mock_decision_logger = mock.patch.object(self.optimizely.decision_service, 'logger') - with mock_decision_logger as mock_decision_logging: - self.optimizely.get_variation( - experiment_key, - user_id, - attributes={'test_attribute': 'wrong_test_value'} - ) - - mock_decision_logging.debug.assert_any_call( - 'User "test_user" is not in the forced variation map.' - ) - mock_decision_logging.info.assert_called_with( - 'User "test_user" does not meet conditions to be in experiment "test_experiment".' - ) - - def test_get_variation__forced_bucketing(self): - """ Test that the expected forced variation is called for a valid experiment and attributes """ - - self.assertTrue(self.optimizely.set_forced_variation('test_experiment', 'test_user', 'variation')) - self.assertEqual('variation', self.optimizely.get_forced_variation('test_experiment', 'test_user')) - variation_key = self.optimizely.get_variation('test_experiment', - 'test_user', - attributes={'test_attribute': 'test_value'}) - self.assertEqual('variation', variation_key) - - 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) as mock_is_experiment_running: - self.optimizely.set_forced_variation('test_experiment', 'test_user', 'variation') - self.assertEqual('variation', self.optimizely.get_forced_variation('test_experiment', 'test_user')) - variation_key = self.optimizely.get_variation('test_experiment', - 'test_user', - attributes={'test_attribute': 'test_value'}) - self.assertIsNone(variation_key) - mock_is_experiment_running.assert_called_once_with(self.project_config.get_experiment_from_key('test_experiment')) - - def test_get_variation__whitelisted_user_forced_bucketing(self): - """ Test that the expected forced variation is called if a user is whitelisted """ - - self.assertTrue(self.optimizely.set_forced_variation('group_exp_1', 'user_1', 'group_exp_1_variation')) - forced_variation = self.optimizely.get_forced_variation('group_exp_1', 'user_1') - self.assertEqual('group_exp_1_variation', forced_variation) - variation_key = self.optimizely.get_variation('group_exp_1', - 'user_1', - attributes={'test_attribute': 'test_value'}) - self.assertEqual('group_exp_1_variation', variation_key) - - 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')): - self.assertTrue(self.optimizely.set_forced_variation('test_experiment', 'test_user', 'variation')) - self.assertEqual('variation', self.optimizely.get_forced_variation('test_experiment', 'test_user')) - variation_key = self.optimizely.get_variation('test_experiment', - 'test_user', - attributes={'test_attribute': 'test_value'}) - self.assertEqual('variation', variation_key) - - def test_get_variation__invalid_attributes__forced_bucketing(self): - """ Test that the expected forced variation is called if the user does not pass audience evaluation """ - - self.assertTrue(self.optimizely.set_forced_variation('test_experiment', 'test_user', 'variation')) - self.assertEqual('variation', self.optimizely.get_forced_variation('test_experiment', 'test_user')) - variation_key = self.optimizely.get_variation('test_experiment', - 'test_user', - attributes={'test_attribute': 'test_value_invalid'}) - self.assertEqual('variation', variation_key) - - def test_set_forced_variation__invalid_object(self): - """ Test that set_forced_variation logs error if Optimizely instance is invalid. """ - - class InvalidConfigManager(object): - pass - - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), config_manager=InvalidConfigManager()) - - with mock.patch.object(opt_obj, 'logger') as mock_client_logging: - self.assertFalse(opt_obj.set_forced_variation('test_experiment', 'test_user', 'test_variation')) - - mock_client_logging.error.assert_called_once_with('Optimizely instance is not valid. ' - 'Failing "set_forced_variation".') - - def test_set_forced_variation__invalid_config(self): - """ Test that set_forced_variation logs error if config is invalid. """ - - opt_obj = optimizely.Optimizely('invalid_datafile') - - with mock.patch.object(opt_obj, 'logger') as mock_client_logging: - self.assertFalse(opt_obj.set_forced_variation('test_experiment', 'test_user', 'test_variation')) - - mock_client_logging.error.assert_called_once_with('Invalid config. Optimizely instance is not valid. ' - 'Failing "set_forced_variation".') - - def test_set_forced_variation__invalid_experiment_key(self): - """ Test that None is returned and expected log messages are logged during set_forced_variation \ + with mock.patch.object(self.optimizely, 'logger') as mock_client_logging: + self.assertIsNone(self.optimizely.activate('test_experiment', 99)) + + mock_client_logging.error.assert_called_once_with('Provided "user_id" is in an invalid format.') + + def test_activate__empty_user_id(self): + """ Test that expected log messages are logged during activate. """ + + variation_key = 'variation' + experiment_key = 'test_experiment' + user_id = '' + + with mock.patch( + '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( + self.optimizely, 'logger' + ) as mock_client_logging: + self.assertEqual(variation_key, self.optimizely.activate(experiment_key, user_id)) + + mock_client_logging.info.assert_called_once_with('Activating user "" in experiment "test_experiment".') + + def test_activate__invalid_attributes(self): + """ Test that expected log messages are logged during activate when attributes are in invalid format. """ + with mock.patch.object(self.optimizely, 'logger') as mock_client_logging: + self.optimizely.activate('test_experiment', 'test_user', attributes='invalid') + + mock_client_logging.error.assert_called_once_with('Provided attributes are in an invalid format.') + mock_client_logging.info.assert_called_once_with('Not activating user "test_user".') + + 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 + ) as mock_is_experiment_running: + self.optimizely.get_variation( + 'test_experiment', 'test_user', attributes={'test_attribute': 'test_value'}, + ) + + mock_decision_logging.info.assert_called_once_with('Experiment "test_experiment" is not running.') + mock_is_experiment_running.assert_called_once_with( + self.project_config.get_experiment_from_key('test_experiment') + ) + + def test_get_variation__no_audience_match(self): + """ Test that expected log messages are logged during get variation when audience conditions are not met. """ + + experiment_key = 'test_experiment' + user_id = 'test_user' + + mock_decision_logger = mock.patch.object(self.optimizely.decision_service, 'logger') + with mock_decision_logger as mock_decision_logging: + self.optimizely.get_variation( + experiment_key, user_id, attributes={'test_attribute': 'wrong_test_value'}, + ) + + mock_decision_logging.debug.assert_any_call('User "test_user" is not in the forced variation map.') + mock_decision_logging.info.assert_called_with( + 'User "test_user" does not meet conditions to be in experiment "test_experiment".' + ) + + def test_get_variation__forced_bucketing(self): + """ Test that the expected forced variation is called for a valid experiment and attributes """ + + self.assertTrue(self.optimizely.set_forced_variation('test_experiment', 'test_user', 'variation')) + self.assertEqual( + 'variation', self.optimizely.get_forced_variation('test_experiment', 'test_user'), + ) + variation_key = self.optimizely.get_variation( + 'test_experiment', 'test_user', attributes={'test_attribute': 'test_value'} + ) + self.assertEqual('variation', variation_key) + + 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 + ) as mock_is_experiment_running: + self.optimizely.set_forced_variation('test_experiment', 'test_user', 'variation') + self.assertEqual( + 'variation', self.optimizely.get_forced_variation('test_experiment', 'test_user'), + ) + variation_key = self.optimizely.get_variation( + 'test_experiment', 'test_user', attributes={'test_attribute': 'test_value'}, + ) + self.assertIsNone(variation_key) + mock_is_experiment_running.assert_called_once_with( + self.project_config.get_experiment_from_key('test_experiment') + ) + + def test_get_variation__whitelisted_user_forced_bucketing(self): + """ Test that the expected forced variation is called if a user is whitelisted """ + + self.assertTrue(self.optimizely.set_forced_variation('group_exp_1', 'user_1', 'group_exp_1_variation')) + forced_variation = self.optimizely.get_forced_variation('group_exp_1', 'user_1') + self.assertEqual('group_exp_1_variation', forced_variation) + variation_key = self.optimizely.get_variation( + 'group_exp_1', 'user_1', attributes={'test_attribute': 'test_value'} + ) + self.assertEqual('group_exp_1_variation', variation_key) + + 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'), + ): + self.assertTrue(self.optimizely.set_forced_variation('test_experiment', 'test_user', 'variation')) + self.assertEqual( + 'variation', self.optimizely.get_forced_variation('test_experiment', 'test_user'), + ) + variation_key = self.optimizely.get_variation( + 'test_experiment', 'test_user', attributes={'test_attribute': 'test_value'}, + ) + self.assertEqual('variation', variation_key) + + def test_get_variation__invalid_attributes__forced_bucketing(self): + """ Test that the expected forced variation is called if the user does not pass audience evaluation """ + + self.assertTrue(self.optimizely.set_forced_variation('test_experiment', 'test_user', 'variation')) + self.assertEqual( + 'variation', self.optimizely.get_forced_variation('test_experiment', 'test_user'), + ) + variation_key = self.optimizely.get_variation( + 'test_experiment', 'test_user', attributes={'test_attribute': 'test_value_invalid'}, + ) + self.assertEqual('variation', variation_key) + + def test_set_forced_variation__invalid_object(self): + """ Test that set_forced_variation logs error if Optimizely instance is invalid. """ + + class InvalidConfigManager(object): + pass + + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), config_manager=InvalidConfigManager()) + + with mock.patch.object(opt_obj, 'logger') as mock_client_logging: + self.assertFalse(opt_obj.set_forced_variation('test_experiment', 'test_user', 'test_variation')) + + mock_client_logging.error.assert_called_once_with( + 'Optimizely instance is not valid. ' 'Failing "set_forced_variation".' + ) + + def test_set_forced_variation__invalid_config(self): + """ Test that set_forced_variation logs error if config is invalid. """ + + opt_obj = optimizely.Optimizely('invalid_datafile') + + with mock.patch.object(opt_obj, 'logger') as mock_client_logging: + self.assertFalse(opt_obj.set_forced_variation('test_experiment', 'test_user', 'test_variation')) + + mock_client_logging.error.assert_called_once_with( + 'Invalid config. Optimizely instance is not valid. ' 'Failing "set_forced_variation".' + ) + + def test_set_forced_variation__invalid_experiment_key(self): + """ Test that None is returned and expected log messages are logged during set_forced_variation \ 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) as mock_validator: - self.assertFalse(self.optimizely.set_forced_variation(99, 'test_user', 'variation')) + with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, mock.patch( + 'optimizely.helpers.validator.is_non_empty_string', return_value=False + ) as mock_validator: + self.assertFalse(self.optimizely.set_forced_variation(99, 'test_user', 'variation')) - mock_validator.assert_any_call(99) + mock_validator.assert_any_call(99) - mock_client_logging.error.assert_called_once_with('Provided "experiment_key" is in an invalid format.') + mock_client_logging.error.assert_called_once_with('Provided "experiment_key" is in an invalid format.') - def test_set_forced_variation__invalid_user_id(self): - """ Test that None is returned and expected log messages are logged during set_forced_variation \ + def test_set_forced_variation__invalid_user_id(self): + """ Test that None is returned and expected log messages are logged during set_forced_variation \ when user_id is in invalid format. """ - with mock.patch.object(self.optimizely, 'logger') as mock_client_logging: - self.assertFalse(self.optimizely.set_forced_variation('test_experiment', 99, 'variation')) - mock_client_logging.error.assert_called_once_with('Provided "user_id" is in an invalid format.') + with mock.patch.object(self.optimizely, 'logger') as mock_client_logging: + self.assertFalse(self.optimizely.set_forced_variation('test_experiment', 99, 'variation')) + mock_client_logging.error.assert_called_once_with('Provided "user_id" is in an invalid format.') - def test_get_forced_variation__invalid_object(self): - """ Test that get_forced_variation logs error if Optimizely instance is invalid. """ + def test_get_forced_variation__invalid_object(self): + """ Test that get_forced_variation logs error if Optimizely instance is invalid. """ - class InvalidConfigManager(object): - pass + class InvalidConfigManager(object): + pass - opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), config_manager=InvalidConfigManager()) + opt_obj = optimizely.Optimizely(json.dumps(self.config_dict), config_manager=InvalidConfigManager()) - with mock.patch.object(opt_obj, 'logger') as mock_client_logging: - self.assertIsNone(opt_obj.get_forced_variation('test_experiment', 'test_user')) + with mock.patch.object(opt_obj, 'logger') as mock_client_logging: + self.assertIsNone(opt_obj.get_forced_variation('test_experiment', 'test_user')) - mock_client_logging.error.assert_called_once_with('Optimizely instance is not valid. ' - 'Failing "get_forced_variation".') + mock_client_logging.error.assert_called_once_with( + 'Optimizely instance is not valid. ' 'Failing "get_forced_variation".' + ) - def test_get_forced_variation__invalid_config(self): - """ Test that get_forced_variation logs error if config is invalid. """ + def test_get_forced_variation__invalid_config(self): + """ Test that get_forced_variation logs error if config is invalid. """ - opt_obj = optimizely.Optimizely('invalid_datafile') + opt_obj = optimizely.Optimizely('invalid_datafile') - with mock.patch.object(opt_obj, 'logger') as mock_client_logging: - self.assertIsNone(opt_obj.get_forced_variation('test_experiment', 'test_user')) + with mock.patch.object(opt_obj, 'logger') as mock_client_logging: + self.assertIsNone(opt_obj.get_forced_variation('test_experiment', 'test_user')) - mock_client_logging.error.assert_called_once_with('Invalid config. Optimizely instance is not valid. ' - 'Failing "get_forced_variation".') + mock_client_logging.error.assert_called_once_with( + 'Invalid config. Optimizely instance is not valid. ' 'Failing "get_forced_variation".' + ) - def test_get_forced_variation__invalid_experiment_key(self): - """ Test that None is returned and expected log messages are logged during get_forced_variation \ + def test_get_forced_variation__invalid_experiment_key(self): + """ Test that None is returned and expected log messages are logged during get_forced_variation \ 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) as mock_validator: - self.assertIsNone(self.optimizely.get_forced_variation(99, 'test_user')) + with mock.patch.object(self.optimizely, 'logger') as mock_client_logging, mock.patch( + 'optimizely.helpers.validator.is_non_empty_string', return_value=False + ) as mock_validator: + self.assertIsNone(self.optimizely.get_forced_variation(99, 'test_user')) - mock_validator.assert_any_call(99) + mock_validator.assert_any_call(99) - mock_client_logging.error.assert_called_once_with('Provided "experiment_key" is in an invalid format.') + mock_client_logging.error.assert_called_once_with('Provided "experiment_key" is in an invalid format.') - def test_get_forced_variation__invalid_user_id(self): - """ Test that None is returned and expected log messages are logged during get_forced_variation \ + def test_get_forced_variation__invalid_user_id(self): + """ Test that None is returned and expected log messages are logged during get_forced_variation \ when user_id is in invalid format. """ - with mock.patch.object(self.optimizely, 'logger') as mock_client_logging: - self.assertIsNone(self.optimizely.get_forced_variation('test_experiment', 99)) + with mock.patch.object(self.optimizely, 'logger') as mock_client_logging: + self.assertIsNone(self.optimizely.get_forced_variation('test_experiment', 99)) - mock_client_logging.error.assert_called_once_with('Provided "user_id" is in an invalid format.') + mock_client_logging.error.assert_called_once_with('Provided "user_id" is in an invalid format.') diff --git a/tests/test_user_event_factory.py b/tests/test_user_event_factory.py index 3c949979..b048bf5b 100644 --- a/tests/test_user_event_factory.py +++ b/tests/test_user_event_factory.py @@ -18,122 +18,105 @@ class UserEventFactoryTest(base.BaseTest): - def setUp(self): - base.BaseTest.setUp(self, 'config_dict_with_multiple_experiments') - self.logger = logger.NoOpLogger() - - def test_impression_event(self): - project_config = self.project_config - experiment = self.project_config.get_experiment_from_key('test_experiment') - variation = self.project_config.get_variation_from_id(experiment.key, '111128') - user_id = 'test_user' - - impression_event = UserEventFactory.create_impression_event( - project_config, - experiment, - '111128', - user_id, - None - ) - - self.assertEqual(self.project_config.project_id, impression_event.event_context.project_id) - self.assertEqual(self.project_config.revision, impression_event.event_context.revision) - self.assertEqual(self.project_config.account_id, impression_event.event_context.account_id) - self.assertEqual(self.project_config.anonymize_ip, impression_event.event_context.anonymize_ip) - self.assertEqual(self.project_config.bot_filtering, impression_event.bot_filtering) - self.assertEqual(experiment, impression_event.experiment) - self.assertEqual(variation, impression_event.variation) - self.assertEqual(user_id, impression_event.user_id) - - def test_impression_event__with_attributes(self): - project_config = self.project_config - experiment = self.project_config.get_experiment_from_key('test_experiment') - variation = self.project_config.get_variation_from_id(experiment.key, '111128') - user_id = 'test_user' - - user_attributes = { - 'test_attribute': 'test_value', - 'boolean_key': True - } - - impression_event = UserEventFactory.create_impression_event( - project_config, - experiment, - '111128', - user_id, - user_attributes - ) - - expected_attrs = EventFactory.build_attribute_list(user_attributes, project_config) - - self.assertEqual(self.project_config.project_id, impression_event.event_context.project_id) - self.assertEqual(self.project_config.revision, impression_event.event_context.revision) - self.assertEqual(self.project_config.account_id, impression_event.event_context.account_id) - self.assertEqual(self.project_config.anonymize_ip, impression_event.event_context.anonymize_ip) - self.assertEqual(self.project_config.bot_filtering, impression_event.bot_filtering) - self.assertEqual(experiment, impression_event.experiment) - self.assertEqual(variation, impression_event.variation) - self.assertEqual(user_id, impression_event.user_id) - self.assertEqual([x.__dict__ for x in expected_attrs], [x.__dict__ for x in impression_event.visitor_attributes]) - - def test_conversion_event(self): - project_config = self.project_config - user_id = 'test_user' - event_key = 'test_event' - user_attributes = { - 'test_attribute': 'test_value', - 'boolean_key': True - } - - conversion_event = UserEventFactory.create_conversion_event( - project_config, - event_key, - user_id, - user_attributes, - None - ) - - expected_attrs = EventFactory.build_attribute_list(user_attributes, project_config) - - self.assertEqual(self.project_config.project_id, conversion_event.event_context.project_id) - self.assertEqual(self.project_config.revision, conversion_event.event_context.revision) - self.assertEqual(self.project_config.account_id, conversion_event.event_context.account_id) - self.assertEqual(self.project_config.anonymize_ip, conversion_event.event_context.anonymize_ip) - self.assertEqual(self.project_config.bot_filtering, conversion_event.bot_filtering) - self.assertEqual(self.project_config.get_event(event_key), conversion_event.event) - self.assertEqual(user_id, conversion_event.user_id) - self.assertEqual([x.__dict__ for x in expected_attrs], [x.__dict__ for x in conversion_event.visitor_attributes]) - - def test_conversion_event__with_event_tags(self): - project_config = self.project_config - user_id = 'test_user' - event_key = 'test_event' - user_attributes = { - 'test_attribute': 'test_value', - 'boolean_key': True - } - event_tags = { - "revenue": 4200, - "value": 1.234, - "non_revenue": "abc" - } - - conversion_event = UserEventFactory.create_conversion_event( - project_config, - event_key, - user_id, - user_attributes, - event_tags - ) - - expected_attrs = EventFactory.build_attribute_list(user_attributes, project_config) - - self.assertEqual(self.project_config.project_id, conversion_event.event_context.project_id) - self.assertEqual(self.project_config.revision, conversion_event.event_context.revision) - self.assertEqual(self.project_config.account_id, conversion_event.event_context.account_id) - self.assertEqual(self.project_config.anonymize_ip, conversion_event.event_context.anonymize_ip) - self.assertEqual(self.project_config.bot_filtering, conversion_event.bot_filtering) - self.assertEqual(self.project_config.get_event(event_key), conversion_event.event) - self.assertEqual(user_id, conversion_event.user_id) - self.assertEqual([x.__dict__ for x in expected_attrs], [x.__dict__ for x in conversion_event.visitor_attributes]) - self.assertEqual(event_tags, conversion_event.event_tags) + def setUp(self): + base.BaseTest.setUp(self, 'config_dict_with_multiple_experiments') + self.logger = logger.NoOpLogger() + + def test_impression_event(self): + project_config = self.project_config + experiment = self.project_config.get_experiment_from_key('test_experiment') + variation = self.project_config.get_variation_from_id(experiment.key, '111128') + user_id = 'test_user' + + impression_event = UserEventFactory.create_impression_event(project_config, experiment, '111128', user_id, None) + + self.assertEqual(self.project_config.project_id, impression_event.event_context.project_id) + self.assertEqual(self.project_config.revision, impression_event.event_context.revision) + self.assertEqual(self.project_config.account_id, impression_event.event_context.account_id) + self.assertEqual( + self.project_config.anonymize_ip, impression_event.event_context.anonymize_ip, + ) + self.assertEqual(self.project_config.bot_filtering, impression_event.bot_filtering) + self.assertEqual(experiment, impression_event.experiment) + self.assertEqual(variation, impression_event.variation) + self.assertEqual(user_id, impression_event.user_id) + + def test_impression_event__with_attributes(self): + project_config = self.project_config + experiment = self.project_config.get_experiment_from_key('test_experiment') + variation = self.project_config.get_variation_from_id(experiment.key, '111128') + user_id = 'test_user' + + user_attributes = {'test_attribute': 'test_value', 'boolean_key': True} + + impression_event = UserEventFactory.create_impression_event( + project_config, experiment, '111128', user_id, user_attributes + ) + + expected_attrs = EventFactory.build_attribute_list(user_attributes, project_config) + + self.assertEqual(self.project_config.project_id, impression_event.event_context.project_id) + self.assertEqual(self.project_config.revision, impression_event.event_context.revision) + self.assertEqual(self.project_config.account_id, impression_event.event_context.account_id) + self.assertEqual( + self.project_config.anonymize_ip, impression_event.event_context.anonymize_ip, + ) + self.assertEqual(self.project_config.bot_filtering, impression_event.bot_filtering) + self.assertEqual(experiment, impression_event.experiment) + self.assertEqual(variation, impression_event.variation) + self.assertEqual(user_id, impression_event.user_id) + self.assertEqual( + [x.__dict__ for x in expected_attrs], [x.__dict__ for x in impression_event.visitor_attributes], + ) + + def test_conversion_event(self): + project_config = self.project_config + user_id = 'test_user' + event_key = 'test_event' + user_attributes = {'test_attribute': 'test_value', 'boolean_key': True} + + conversion_event = UserEventFactory.create_conversion_event( + project_config, event_key, user_id, user_attributes, None + ) + + expected_attrs = EventFactory.build_attribute_list(user_attributes, project_config) + + self.assertEqual(self.project_config.project_id, conversion_event.event_context.project_id) + self.assertEqual(self.project_config.revision, conversion_event.event_context.revision) + self.assertEqual(self.project_config.account_id, conversion_event.event_context.account_id) + self.assertEqual( + self.project_config.anonymize_ip, conversion_event.event_context.anonymize_ip, + ) + self.assertEqual(self.project_config.bot_filtering, conversion_event.bot_filtering) + self.assertEqual(self.project_config.get_event(event_key), conversion_event.event) + self.assertEqual(user_id, conversion_event.user_id) + self.assertEqual( + [x.__dict__ for x in expected_attrs], [x.__dict__ for x in conversion_event.visitor_attributes], + ) + + def test_conversion_event__with_event_tags(self): + project_config = self.project_config + user_id = 'test_user' + event_key = 'test_event' + user_attributes = {'test_attribute': 'test_value', 'boolean_key': True} + event_tags = {"revenue": 4200, "value": 1.234, "non_revenue": "abc"} + + conversion_event = UserEventFactory.create_conversion_event( + project_config, event_key, user_id, user_attributes, event_tags + ) + + expected_attrs = EventFactory.build_attribute_list(user_attributes, project_config) + + self.assertEqual(self.project_config.project_id, conversion_event.event_context.project_id) + self.assertEqual(self.project_config.revision, conversion_event.event_context.revision) + self.assertEqual(self.project_config.account_id, conversion_event.event_context.account_id) + self.assertEqual( + self.project_config.anonymize_ip, conversion_event.event_context.anonymize_ip, + ) + self.assertEqual(self.project_config.bot_filtering, conversion_event.bot_filtering) + self.assertEqual(self.project_config.get_event(event_key), conversion_event.event) + self.assertEqual(user_id, conversion_event.user_id) + self.assertEqual( + [x.__dict__ for x in expected_attrs], [x.__dict__ for x in conversion_event.visitor_attributes], + ) + self.assertEqual(event_tags, conversion_event.event_tags) diff --git a/tests/test_user_profile.py b/tests/test_user_profile.py index 9b110588..ffeb3e34 100644 --- a/tests/test_user_profile.py +++ b/tests/test_user_profile.py @@ -17,51 +17,49 @@ class UserProfileTest(unittest.TestCase): + def setUp(self): + user_id = 'test_user' + experiment_bucket_map = {'199912': {'variation_id': '14512525'}} - def setUp(self): - user_id = 'test_user' - experiment_bucket_map = { - '199912': { - 'variation_id': '14512525' - } - } + self.profile = user_profile.UserProfile(user_id, experiment_bucket_map=experiment_bucket_map) - self.profile = user_profile.UserProfile(user_id, experiment_bucket_map=experiment_bucket_map) + def test_get_variation_for_experiment__decision_exists(self): + """ Test that variation ID is retrieved correctly if a decision exists in the experiment bucket map. """ - def test_get_variation_for_experiment__decision_exists(self): - """ Test that variation ID is retrieved correctly if a decision exists in the experiment bucket map. """ + self.assertEqual('14512525', self.profile.get_variation_for_experiment('199912')) - self.assertEqual('14512525', self.profile.get_variation_for_experiment('199912')) + def test_get_variation_for_experiment__no_decision_exists(self): + """ Test that None is returned if no decision exists in the experiment bucket map. """ - def test_get_variation_for_experiment__no_decision_exists(self): - """ Test that None is returned if no decision exists in the experiment bucket map. """ + self.assertIsNone(self.profile.get_variation_for_experiment('199924')) - self.assertIsNone(self.profile.get_variation_for_experiment('199924')) + def test_set_variation_for_experiment__no_previous_decision(self): + """ Test that decision for new experiment/variation is stored correctly. """ - def test_set_variation_for_experiment__no_previous_decision(self): - """ Test that decision for new experiment/variation is stored correctly. """ + self.profile.save_variation_for_experiment('1993412', '118822') + self.assertEqual( + {'199912': {'variation_id': '14512525'}, '1993412': {'variation_id': '118822'}}, + self.profile.experiment_bucket_map, + ) - self.profile.save_variation_for_experiment('1993412', '118822') - self.assertEqual({'199912': {'variation_id': '14512525'}, - '1993412': {'variation_id': '118822'}}, self.profile.experiment_bucket_map) + def test_set_variation_for_experiment__previous_decision_available(self): + """ Test that decision for is updated correctly if new experiment/variation combination is available. """ - def test_set_variation_for_experiment__previous_decision_available(self): - """ Test that decision for is updated correctly if new experiment/variation combination is available. """ - - self.profile.save_variation_for_experiment('199912', '1224525') - self.assertEqual({'199912': {'variation_id': '1224525'}}, self.profile.experiment_bucket_map) + self.profile.save_variation_for_experiment('199912', '1224525') + self.assertEqual({'199912': {'variation_id': '1224525'}}, self.profile.experiment_bucket_map) class UserProfileServiceTest(unittest.TestCase): + def test_lookup(self): + """ Test that lookup returns user profile in expected format. """ - def test_lookup(self): - """ Test that lookup returns user profile in expected format. """ - - user_profile_service = user_profile.UserProfileService() - self.assertEqual({'user_id': 'test_user', 'experiment_bucket_map': {}}, user_profile_service.lookup('test_user')) + user_profile_service = user_profile.UserProfileService() + self.assertEqual( + {'user_id': 'test_user', 'experiment_bucket_map': {}}, user_profile_service.lookup('test_user'), + ) - def test_save(self): - """ Test that nothing happens on calling save. """ + def test_save(self): + """ Test that nothing happens on calling save. """ - user_profile_service = user_profile.UserProfileService() - self.assertIsNone(user_profile_service.save({'user_id': 'test_user', 'experiment_bucket_map': {}})) + user_profile_service = user_profile.UserProfileService() + self.assertIsNone(user_profile_service.save({'user_id': 'test_user', 'experiment_bucket_map': {}})) diff --git a/tests/testapp/application.py b/tests/testapp/application.py index 5077e978..7b2a81ee 100644 --- a/tests/testapp/application.py +++ b/tests/testapp/application.py @@ -36,298 +36,383 @@ def copy_func(f, name=None): - return types.FunctionType(f.func_code, f.func_globals, name or f.func_name, - f.func_defaults, f.func_closure) + return types.FunctionType(f.func_code, f.func_globals, name or f.func_name, f.func_defaults, f.func_closure,) def on_activate(experiment, _user_id, _attributes, variation, event): - # listener callback for activate. - global listener_return_maps + # listener callback for activate. + global listener_return_maps - listener_return_map = {'experiment_key': experiment.key, 'user_id': _user_id, - 'attributes': _attributes or {}, - 'variation_key': variation.key} + listener_return_map = { + 'experiment_key': experiment.key, + 'user_id': _user_id, + 'attributes': _attributes or {}, + 'variation_key': variation.key, + } - if listener_return_maps is None: - listener_return_maps = [listener_return_map] - else: - listener_return_maps.append(listener_return_map) + if listener_return_maps is None: + listener_return_maps = [listener_return_map] + else: + listener_return_maps.append(listener_return_map) def on_track(_event_key, _user_id, _attributes, _event_tags, event): - # listener callback for track - global listener_return_maps + # listener callback for track + global listener_return_maps - listener_return_map = {'event_key': _event_key, "user_id": _user_id, - 'attributes': _attributes or {}, - 'event_tags': _event_tags or {}} - if listener_return_maps is None: - listener_return_maps = [listener_return_map] - else: - listener_return_maps.append(listener_return_map) + listener_return_map = { + 'event_key': _event_key, + "user_id": _user_id, + 'attributes': _attributes or {}, + 'event_tags': _event_tags or {}, + } + if listener_return_maps is None: + listener_return_maps = [listener_return_map] + else: + listener_return_maps.append(listener_return_map) @app.before_request def before_request(): - global user_profile_service_instance - global optimizely_instance - - user_profile_service_instance = None - optimizely_instance = None - - request.payload = request.get_json() - user_profile_service_instance = request.payload.get('user_profile_service') - if user_profile_service_instance: - ups_class = getattr(user_profile_service, request.payload.get('user_profile_service')) - user_profile_service_instance = ups_class(request.payload.get('user_profiles')) - - with_listener = request.payload.get('with_listener') - - log_level = environ.get('OPTIMIZELY_SDK_LOG_LEVEL', 'DEBUG') - min_level = getattr(logging, log_level) - optimizely_instance = optimizely.Optimizely(datafile_content, logger=logger.SimpleLogger(min_level=min_level), - user_profile_service=user_profile_service_instance) - - if with_listener is not None: - for listener_add in with_listener: - if listener_add['type'] == 'Activate': - count = int(listener_add['count']) - for i in range(count): - # make a value copy so that we can add multiple callbacks. - a_cb = copy_func(on_activate) - optimizely_instance.notification_center.add_notification_listener(enums.NotificationTypes.ACTIVATE, a_cb) - if listener_add['type'] == 'Track': - count = int(listener_add['count']) - for i in range(count): - # make a value copy so that we can add multiple callbacks. - t_cb = copy_func(on_track) - optimizely_instance.notification_center.add_notification_listener(enums.NotificationTypes.TRACK, t_cb) + global user_profile_service_instance + global optimizely_instance + + user_profile_service_instance = None + optimizely_instance = None + + request.payload = request.get_json() + user_profile_service_instance = request.payload.get('user_profile_service') + if user_profile_service_instance: + ups_class = getattr(user_profile_service, request.payload.get('user_profile_service')) + user_profile_service_instance = ups_class(request.payload.get('user_profiles')) + + with_listener = request.payload.get('with_listener') + + log_level = environ.get('OPTIMIZELY_SDK_LOG_LEVEL', 'DEBUG') + min_level = getattr(logging, log_level) + optimizely_instance = optimizely.Optimizely( + datafile_content, + logger=logger.SimpleLogger(min_level=min_level), + user_profile_service=user_profile_service_instance, + ) + + if with_listener is not None: + for listener_add in with_listener: + if listener_add['type'] == 'Activate': + count = int(listener_add['count']) + for i in range(count): + # make a value copy so that we can add multiple callbacks. + a_cb = copy_func(on_activate) + optimizely_instance.notification_center.add_notification_listener( + enums.NotificationTypes.ACTIVATE, a_cb + ) + if listener_add['type'] == 'Track': + count = int(listener_add['count']) + for i in range(count): + # make a value copy so that we can add multiple callbacks. + t_cb = copy_func(on_track) + optimizely_instance.notification_center.add_notification_listener( + enums.NotificationTypes.TRACK, t_cb + ) @app.after_request def after_request(response): - global optimizely_instance - global listener_return_maps + global optimizely_instance + global listener_return_maps - optimizely_instance.notification_center.clear_all_notifications() - listener_return_maps = None - return response + optimizely_instance.notification_center.clear_all_notifications() + listener_return_maps = None + return response @app.route('/activate', methods=['POST']) def activate(): - payload = request.get_json() - experiment_key = payload.get('experiment_key') - user_id = payload.get('user_id') - attributes = payload.get('attributes') + payload = request.get_json() + experiment_key = payload.get('experiment_key') + user_id = payload.get('user_id') + attributes = payload.get('attributes') - variation = optimizely_instance.activate(experiment_key, user_id, attributes=attributes) - user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else [] + variation = optimizely_instance.activate(experiment_key, user_id, attributes=attributes) + user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else [] - payload = {'result': variation, 'user_profiles': user_profiles, 'listener_called': listener_return_maps} - return json.dumps(payload), 200, {'content-type': 'application/json'} + payload = { + 'result': variation, + 'user_profiles': user_profiles, + 'listener_called': listener_return_maps, + } + return json.dumps(payload), 200, {'content-type': 'application/json'} @app.route('/get_variation', methods=['POST']) def get_variation(): - payload = request.get_json() - experiment_key = payload.get('experiment_key') - user_id = payload.get('user_id') - attributes = payload.get('attributes') - variation = optimizely_instance.get_variation(experiment_key, user_id, attributes=attributes) - user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else [] - return json.dumps({'result': variation, 'user_profiles': user_profiles}), 200, {'content-type': 'application/json'} + payload = request.get_json() + experiment_key = payload.get('experiment_key') + user_id = payload.get('user_id') + attributes = payload.get('attributes') + variation = optimizely_instance.get_variation(experiment_key, user_id, attributes=attributes) + user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else [] + return ( + json.dumps({'result': variation, 'user_profiles': user_profiles}), + 200, + {'content-type': 'application/json'}, + ) @app.route('/track', methods=['POST']) def track(): - payload = request.get_json() - event_key = payload.get('event_key') - user_id = payload.get('user_id') - attributes = payload.get('attributes') - event_tags = payload.get('event_tags') + payload = request.get_json() + event_key = payload.get('event_key') + user_id = payload.get('user_id') + attributes = payload.get('attributes') + event_tags = payload.get('event_tags') - result = optimizely_instance.track(event_key, user_id, attributes, event_tags) + result = optimizely_instance.track(event_key, user_id, attributes, event_tags) - user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else [] + user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else [] - payload = {'result': result, 'user_profiles': user_profiles, 'listener_called': listener_return_maps} - return json.dumps(payload), 200, {'content-type': 'application/json'} + payload = { + 'result': result, + 'user_profiles': user_profiles, + 'listener_called': listener_return_maps, + } + return json.dumps(payload), 200, {'content-type': 'application/json'} @app.route('/is_feature_enabled', methods=['POST']) def is_feature_enabled(): - payload = request.get_json() - feature_flag_key = payload.get('feature_flag_key') - user_id = payload.get('user_id') - attributes = payload.get('attributes') + payload = request.get_json() + feature_flag_key = payload.get('feature_flag_key') + user_id = payload.get('user_id') + attributes = payload.get('attributes') - feature_enabled = optimizely_instance.is_feature_enabled(feature_flag_key, user_id, attributes) - user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else {} + feature_enabled = optimizely_instance.is_feature_enabled(feature_flag_key, user_id, attributes) + user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else {} - result = feature_enabled if feature_enabled is None else 'true' if feature_enabled is True else 'false' - return json.dumps({'result': result, 'user_profiles': user_profiles}), 200, {'content-type': 'application/json'} + result = feature_enabled if feature_enabled is None else 'true' if feature_enabled is True else 'false' + return ( + json.dumps({'result': result, 'user_profiles': user_profiles}), + 200, + {'content-type': 'application/json'}, + ) @app.route('/get_enabled_features', methods=['POST']) def get_enabled_features(): - payload = request.get_json() - user_id = payload.get('user_id') - attributes = payload.get('attributes') + payload = request.get_json() + user_id = payload.get('user_id') + attributes = payload.get('attributes') - enabled_features = optimizely_instance.get_enabled_features(user_id, attributes) - user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else {} + enabled_features = optimizely_instance.get_enabled_features(user_id, attributes) + user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else {} - payload = {'result': enabled_features, 'user_profiles': user_profiles, 'listener_called': listener_return_maps} - return json.dumps(payload), 200, {'content-type': 'application/json'} + payload = { + 'result': enabled_features, + 'user_profiles': user_profiles, + 'listener_called': listener_return_maps, + } + return json.dumps(payload), 200, {'content-type': 'application/json'} @app.route('/get_feature_variable_boolean', methods=['POST']) def get_feature_variable_boolean(): - payload = request.get_json() - feature_flag_key = payload.get('feature_flag_key') - variable_key = payload.get('variable_key') - user_id = payload.get('user_id') - attributes = payload.get('attributes') - - boolean_value = optimizely_instance.get_feature_variable_boolean(feature_flag_key, - variable_key, - user_id, - attributes) - user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else {} - return json.dumps({'result': boolean_value, - 'user_profiles': user_profiles}), 200, {'content-type': 'application/json'} + payload = request.get_json() + feature_flag_key = payload.get('feature_flag_key') + variable_key = payload.get('variable_key') + user_id = payload.get('user_id') + attributes = payload.get('attributes') + + boolean_value = optimizely_instance.get_feature_variable_boolean( + feature_flag_key, variable_key, user_id, attributes + ) + user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else {} + return ( + json.dumps({'result': boolean_value, 'user_profiles': user_profiles}), + 200, + {'content-type': 'application/json'}, + ) @app.route('/get_feature_variable_double', methods=['POST']) def get_feature_variable_double(): - payload = request.get_json() - feature_flag_key = payload.get('feature_flag_key') - variable_key = payload.get('variable_key') - user_id = payload.get('user_id') - attributes = payload.get('attributes') + payload = request.get_json() + feature_flag_key = payload.get('feature_flag_key') + variable_key = payload.get('variable_key') + user_id = payload.get('user_id') + attributes = payload.get('attributes') - double_value = optimizely_instance.get_feature_variable_double(feature_flag_key, - variable_key, - user_id, - attributes) + double_value = optimizely_instance.get_feature_variable_double(feature_flag_key, variable_key, user_id, attributes) - user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else {} - return json.dumps({'result': double_value, - 'user_profiles': user_profiles}), 200, {'content-type': 'application/json'} + user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else {} + return ( + json.dumps({'result': double_value, 'user_profiles': user_profiles}), + 200, + {'content-type': 'application/json'}, + ) @app.route('/get_feature_variable_integer', methods=['POST']) def get_feature_variable_integer(): - payload = request.get_json() - feature_flag_key = payload.get('feature_flag_key') - variable_key = payload.get('variable_key') - user_id = payload.get('user_id') - attributes = payload.get('attributes') + payload = request.get_json() + feature_flag_key = payload.get('feature_flag_key') + variable_key = payload.get('variable_key') + user_id = payload.get('user_id') + attributes = payload.get('attributes') - integer_value = optimizely_instance.get_feature_variable_integer(feature_flag_key, - variable_key, - user_id, - attributes) + integer_value = optimizely_instance.get_feature_variable_integer( + feature_flag_key, variable_key, user_id, attributes + ) - user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else {} - return json.dumps({'result': integer_value, - 'user_profiles': user_profiles}), 200, {'content-type': 'application/json'} + user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else {} + return ( + json.dumps({'result': integer_value, 'user_profiles': user_profiles}), + 200, + {'content-type': 'application/json'}, + ) @app.route('/get_feature_variable_string', methods=['POST']) def get_feature_variable_string(): - payload = request.get_json() - feature_flag_key = payload.get('feature_flag_key') - variable_key = payload.get('variable_key') - user_id = payload.get('user_id') - attributes = payload.get('attributes') + payload = request.get_json() + feature_flag_key = payload.get('feature_flag_key') + variable_key = payload.get('variable_key') + user_id = payload.get('user_id') + attributes = payload.get('attributes') - string_value = optimizely_instance.get_feature_variable_string(feature_flag_key, - variable_key, - user_id, - attributes) + string_value = optimizely_instance.get_feature_variable_string(feature_flag_key, variable_key, user_id, attributes) - user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else {} - return json.dumps({'result': string_value, 'user_profiles': user_profiles}), 200, {'content-type': 'application/json'} + user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else {} + return ( + json.dumps({'result': string_value, 'user_profiles': user_profiles}), + 200, + {'content-type': 'application/json'}, + ) @app.route('/forced_variation', methods=['POST']) def forced_variation(): - payload = request.get_json() - user_id = payload.get('user_id') - experiment_key = payload.get('experiment_key') - forced_variation_key = payload.get('forced_variation_key') - user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else [] - result = optimizely_instance.set_forced_variation(experiment_key, user_id, forced_variation_key) - if result is False: - return json.dumps({'result': None, 'user_profiles': user_profiles}), 400, {'content-type': 'application/json'} - variation = optimizely_instance.get_forced_variation(experiment_key, user_id) - return json.dumps({'result': variation, 'user_profiles': user_profiles}), 200, {'content-type': 'application/json'} + payload = request.get_json() + user_id = payload.get('user_id') + experiment_key = payload.get('experiment_key') + forced_variation_key = payload.get('forced_variation_key') + user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else [] + result = optimizely_instance.set_forced_variation(experiment_key, user_id, forced_variation_key) + if result is False: + return ( + json.dumps({'result': None, 'user_profiles': user_profiles}), + 400, + {'content-type': 'application/json'}, + ) + variation = optimizely_instance.get_forced_variation(experiment_key, user_id) + return ( + json.dumps({'result': variation, 'user_profiles': user_profiles}), + 200, + {'content-type': 'application/json'}, + ) @app.route('/forced_variation_multiple_sets', methods=['POST']) def forced_variation_multiple_sets(): - payload = request.get_json() - user_id_1 = payload.get('user_id_1') - user_id_2 = payload.get('user_id_2') - experiment_key_1 = payload.get('experiment_key_1') - experiment_key_2 = payload.get('experiment_key_2') - forced_variation_key_1 = payload.get('forced_variation_key_1') - forced_variation_key_2 = payload.get('forced_variation_key_2') - user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else [] - result = optimizely_instance.set_forced_variation(experiment_key_1, user_id_1, forced_variation_key_1) - if result is False: - return json.dumps({'result': None, 'user_profiles': user_profiles}), 400, {'content-type': 'application/json'} - result = optimizely_instance.set_forced_variation(experiment_key_2, user_id_1, forced_variation_key_2) - if result is False: - return json.dumps({'result': None, 'user_profiles': user_profiles}), 400, {'content-type': 'application/json'} - result = optimizely_instance.set_forced_variation(experiment_key_1, user_id_2, forced_variation_key_1) - if result is False: - return json.dumps({'result': None, 'user_profiles': user_profiles}), 400, {'content-type': 'application/json'} - result = optimizely_instance.set_forced_variation(experiment_key_2, user_id_2, forced_variation_key_2) - if result is False: - return json.dumps({'result': None, 'user_profiles': user_profiles}), 400, {'content-type': 'application/json'} - variation_1 = optimizely_instance.get_forced_variation(experiment_key_1, user_id_1) - variation_2 = optimizely_instance.get_forced_variation(experiment_key_2, user_id_1) - variation_3 = optimizely_instance.get_forced_variation(experiment_key_1, user_id_2) - variation_4 = optimizely_instance.get_forced_variation(experiment_key_2, user_id_2) - return json.dumps({'result_1': variation_1, - 'result_2': variation_2, - 'result_3': variation_3, - 'result_4': variation_4, - 'user_profiles': user_profiles}), 200, {'content-type': 'application/json'} + payload = request.get_json() + user_id_1 = payload.get('user_id_1') + user_id_2 = payload.get('user_id_2') + experiment_key_1 = payload.get('experiment_key_1') + experiment_key_2 = payload.get('experiment_key_2') + forced_variation_key_1 = payload.get('forced_variation_key_1') + forced_variation_key_2 = payload.get('forced_variation_key_2') + user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else [] + result = optimizely_instance.set_forced_variation(experiment_key_1, user_id_1, forced_variation_key_1) + if result is False: + return ( + json.dumps({'result': None, 'user_profiles': user_profiles}), + 400, + {'content-type': 'application/json'}, + ) + result = optimizely_instance.set_forced_variation(experiment_key_2, user_id_1, forced_variation_key_2) + if result is False: + return ( + json.dumps({'result': None, 'user_profiles': user_profiles}), + 400, + {'content-type': 'application/json'}, + ) + result = optimizely_instance.set_forced_variation(experiment_key_1, user_id_2, forced_variation_key_1) + if result is False: + return ( + json.dumps({'result': None, 'user_profiles': user_profiles}), + 400, + {'content-type': 'application/json'}, + ) + result = optimizely_instance.set_forced_variation(experiment_key_2, user_id_2, forced_variation_key_2) + if result is False: + return ( + json.dumps({'result': None, 'user_profiles': user_profiles}), + 400, + {'content-type': 'application/json'}, + ) + variation_1 = optimizely_instance.get_forced_variation(experiment_key_1, user_id_1) + variation_2 = optimizely_instance.get_forced_variation(experiment_key_2, user_id_1) + variation_3 = optimizely_instance.get_forced_variation(experiment_key_1, user_id_2) + variation_4 = optimizely_instance.get_forced_variation(experiment_key_2, user_id_2) + return ( + json.dumps( + { + 'result_1': variation_1, + 'result_2': variation_2, + 'result_3': variation_3, + 'result_4': variation_4, + 'user_profiles': user_profiles, + } + ), + 200, + {'content-type': 'application/json'}, + ) @app.route('/forced_variation_get_variation', methods=['POST']) def forced_variation_get_variation(): - payload = request.get_json() - user_id = payload.get('user_id') - attributes = payload.get('attributes') - experiment_key = payload.get('experiment_key') - forced_variation_key = payload.get('forced_variation_key') - user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else [] - result = optimizely_instance.set_forced_variation(experiment_key, user_id, forced_variation_key) - if result is False: - return json.dumps({'result': None, 'user_profiles': user_profiles}), 400, {'content-type': 'application/json'} - variation = optimizely_instance.get_variation(experiment_key, user_id, attributes=attributes) - return json.dumps({'result': variation, 'user_profiles': user_profiles}), 200, {'content-type': 'application/json'} + payload = request.get_json() + user_id = payload.get('user_id') + attributes = payload.get('attributes') + experiment_key = payload.get('experiment_key') + forced_variation_key = payload.get('forced_variation_key') + user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else [] + result = optimizely_instance.set_forced_variation(experiment_key, user_id, forced_variation_key) + if result is False: + return ( + json.dumps({'result': None, 'user_profiles': user_profiles}), + 400, + {'content-type': 'application/json'}, + ) + variation = optimizely_instance.get_variation(experiment_key, user_id, attributes=attributes) + return ( + json.dumps({'result': variation, 'user_profiles': user_profiles}), + 200, + {'content-type': 'application/json'}, + ) @app.route('/forced_variation_activate', methods=['POST']) def forced_variation_activate(): - payload = request.get_json() - user_id = payload.get('user_id') - attributes = payload.get('attributes') - experiment_key = payload.get('experiment_key') - forced_variation_key = payload.get('forced_variation_key') - user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else [] - result = optimizely_instance.set_forced_variation(experiment_key, user_id, forced_variation_key) - if result is False: - return json.dumps({'result': None, 'user_profiles': user_profiles}), 400, {'content-type': 'application/json'} - variation = optimizely_instance.activate(experiment_key, user_id, attributes=attributes) - return json.dumps({'result': variation, 'user_profiles': user_profiles}), 200, {'content-type': 'application/json'} + payload = request.get_json() + user_id = payload.get('user_id') + attributes = payload.get('attributes') + experiment_key = payload.get('experiment_key') + forced_variation_key = payload.get('forced_variation_key') + user_profiles = user_profile_service_instance.user_profiles.values() if user_profile_service_instance else [] + result = optimizely_instance.set_forced_variation(experiment_key, user_id, forced_variation_key) + if result is False: + return ( + json.dumps({'result': None, 'user_profiles': user_profiles}), + 400, + {'content-type': 'application/json'}, + ) + variation = optimizely_instance.activate(experiment_key, user_id, attributes=attributes) + return ( + json.dumps({'result': variation, 'user_profiles': user_profiles}), + 200, + {'content-type': 'application/json'}, + ) if __name__ == '__main__': - app.run(host='0.0.0.0', port=3000) + app.run(host='0.0.0.0', port=3000) diff --git a/tests/testapp/user_profile_service.py b/tests/testapp/user_profile_service.py index 9c01374e..144697e5 100644 --- a/tests/testapp/user_profile_service.py +++ b/tests/testapp/user_profile_service.py @@ -13,24 +13,24 @@ class BaseUserProfileService(object): - def __init__(self, user_profiles): - self.user_profiles = {profile['user_id']: profile for profile in user_profiles} if user_profiles else {} + def __init__(self, user_profiles): + self.user_profiles = {profile['user_id']: profile for profile in user_profiles} if user_profiles else {} class NormalService(BaseUserProfileService): - def lookup(self, user_id): - return self.user_profiles.get(user_id) + def lookup(self, user_id): + return self.user_profiles.get(user_id) - def save(self, user_profile): - user_id = user_profile['user_id'] - self.user_profiles[user_id] = user_profile + def save(self, user_profile): + user_id = user_profile['user_id'] + self.user_profiles[user_id] = user_profile class LookupErrorService(NormalService): - def lookup(self, user_id): - raise IOError + def lookup(self, user_id): + raise IOError class SaveErrorService(NormalService): - def save(self, user_profile): - raise IOError + def save(self, user_profile): + raise IOError diff --git a/tox.ini b/tox.ini deleted file mode 100644 index 2c9c6f1c..00000000 --- a/tox.ini +++ /dev/null @@ -1,9 +0,0 @@ -[flake8] -# E111 - indentation is not a multiple of four -# E114 - indentation is not a multiple of four (comment) -# E121 - continuation line indentation is not a multiple of four -# E127 - continuation line over-indented for visual indent -# E722 - do not use bare 'except' -ignore = E111,E114,E121,E127,E722 -exclude = optimizely/lib/pymmh3.py,*virtualenv* -max-line-length = 120