diff --git a/optimizely/helpers/condition.py b/optimizely/helpers/condition.py index 0676aecb..455e09aa 100644 --- a/optimizely/helpers/condition.py +++ b/optimizely/helpers/condition.py @@ -18,6 +18,8 @@ from . import validator from .enums import CommonAudienceEvaluationLogs as audience_logs +from .enums import Errors +from .enums import VersionType class ConditionOperatorTypes(object): @@ -30,7 +32,14 @@ class ConditionMatchTypes(object): EXACT = 'exact' EXISTS = 'exists' GREATER_THAN = 'gt' + GREATER_THAN_OR_EQUAL = 'ge' LESS_THAN = 'lt' + LESS_THAN_OR_EQUAL = 'le' + SEMVER_EQ = 'semver_eq' + SEMVER_GE = 'semver_ge' + SEMVER_GT = 'semver_gt' + SEMVER_LE = 'semver_le' + SEMVER_LT = 'semver_lt' SUBSTRING = 'substring' @@ -84,6 +93,112 @@ def is_value_a_number(self, value): return False + def is_pre_release_version(self, version): + """ Method to check if given version is pre-release. + Criteria for pre-release includes: + - Version includes "-" + + Args: + version: Given version in string. + + Returns: + Boolean: + - True if the given version is pre-release + - False if it doesn't + """ + if VersionType.IS_PRE_RELEASE in version: + user_version_release_index = version.find(VersionType.IS_PRE_RELEASE) + user_version_build_index = version.find(VersionType.IS_BUILD) + if (user_version_release_index < user_version_build_index) or (user_version_build_index < 0): + return True + return False + + def is_build_version(self, version): + """ Method to check given version is a build version. + Criteria for build version includes: + - Version includes "+" + + Args: + version: Given version in string. + + Returns: + Boolean: + - True if the given version is a build version + - False if it doesn't + """ + if VersionType.IS_BUILD in version: + user_version_release_index = version.find(VersionType.IS_PRE_RELEASE) + user_version_build_index = version.find(VersionType.IS_BUILD) + if (user_version_build_index < user_version_release_index) or (user_version_release_index < 0): + return True + return False + + def has_white_space(self, version): + """ Method to check if the given version contains " " (white space) + + Args: + version: Given version in string. + + Returns: + Boolean: + - True if the given version does contain whitespace + - False if it doesn't + """ + return ' ' in version + + def compare_user_version_with_target_version(self, target_version, user_version): + """ Method to compare user version with target version. + + Args: + target_version: String representing condition value + user_version: String representing user value + + Returns: + Int: + - 0 if user version is equal to target version. + - 1 if user version is greater than target version. + - -1 if user version is less than target version or, in case of exact string match, doesn't match the target + version. + None: + - if the user version value format is not a valid semantic version. + """ + is_pre_release_in_target_version = self.is_pre_release_version(target_version) + is_pre_release_in_user_version = self.is_pre_release_version(user_version) + is_build_in_target_version = self.is_build_version(target_version) + + target_version_parts = self.split_version(target_version) + if target_version_parts is None: + return None + + user_version_parts = self.split_version(user_version) + if user_version_parts is None: + return None + + user_version_parts_len = len(user_version_parts) + + for (idx, _) in enumerate(target_version_parts): + if user_version_parts_len <= idx: + return 1 if is_pre_release_in_target_version or is_build_in_target_version else -1 + elif not user_version_parts[idx].isdigit(): + if user_version_parts[idx] < target_version_parts[idx]: + return 1 if is_pre_release_in_target_version and not \ + is_pre_release_in_user_version else -1 + elif user_version_parts[idx] > target_version_parts[idx]: + return -1 if not is_pre_release_in_target_version and \ + is_pre_release_in_user_version else 1 + else: + user_version_part = int(user_version_parts[idx]) + target_version_part = int(target_version_parts[idx]) + if user_version_part > target_version_part: + return 1 + elif user_version_part < target_version_part: + return -1 + + # check if user version contains build or pre-release and target version doesn't + if is_pre_release_in_user_version and is_build_in_target_version: + return -1 + return 0 + def exact_evaluator(self, index): """ Evaluate the given exact match condition for the user attributes. @@ -171,6 +286,40 @@ def greater_than_evaluator(self, index): return user_value > condition_value + def greater_than_or_equal_evaluator(self, index): + """ Evaluate the given greater than or equal to match condition for the user attributes. + + Args: + index: Index of the condition to be evaluated. + + Returns: + Boolean: + - True if the user attribute value is greater than or equal to the condition value. + - False if the user attribute value is less than 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. @@ -205,6 +354,40 @@ def less_than_evaluator(self, index): return user_value < condition_value + def less_than_or_equal_evaluator(self, index): + """ Evaluate the given less than or equal to match condition for the user attributes. + + Args: + index: Index of the condition to be evaluated. + + Returns: + Boolean: + - True if the user attribute value is less than or equal to the condition value. + - False if the user attribute value is greater than 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. @@ -233,14 +416,251 @@ def substring_evaluator(self, index): return condition_value in user_value + def semver_equal_evaluator(self, index): + """ Evaluate the given semantic version equal match target version for the user version. + + Args: + index: Index of the condition to be evaluated. + + Returns: + Boolean: + - True if the user version is equal (==) to the target version. + - False if the user version is not equal (!=) to the target version. + None: + - if the user version value is not string type or is null. + """ + + condition_name = self.condition_data[index][0] + target_version = self.condition_data[index][1] + user_version = self.attributes.get(condition_name) + + if not isinstance(target_version, string_types): + self.logger.warning(audience_logs.UNKNOWN_CONDITION_VALUE.format(self._get_condition_json(index), )) + return None + + if not isinstance(user_version, string_types): + self.logger.warning( + audience_logs.UNEXPECTED_TYPE.format( + self._get_condition_json(index), type(user_version), condition_name + ) + ) + return None + + result = self.compare_user_version_with_target_version(target_version, user_version) + if result is None: + return None + + return result == 0 + + def semver_greater_than_evaluator(self, index): + """ Evaluate the given semantic version greater than match target version for the user version. + + Args: + index: Index of the condition to be evaluated. + + Returns: + Boolean: + - True if the user version is greater than the target version. + - False if the user version is less than or equal to the target version. + None: + - if the user version value is not string type or is null. + """ + condition_name = self.condition_data[index][0] + target_version = self.condition_data[index][1] + user_version = self.attributes.get(condition_name) + + if not isinstance(target_version, string_types): + self.logger.warning(audience_logs.UNKNOWN_CONDITION_VALUE.format(self._get_condition_json(index), )) + return None + + if not isinstance(user_version, string_types): + self.logger.warning( + audience_logs.UNEXPECTED_TYPE.format( + self._get_condition_json(index), type(user_version), condition_name + ) + ) + return None + + result = self.compare_user_version_with_target_version(target_version, user_version) + if result is None: + return None + + return result > 0 + + def semver_less_than_evaluator(self, index): + """ Evaluate the given semantic version less than match target version for the user version. + + Args: + index: Index of the condition to be evaluated. + + Returns: + Boolean: + - True if the user version is less than the target version. + - False if the user version is greater than or equal to the target version. + None: + - if the user version value is not string type or is null. + """ + condition_name = self.condition_data[index][0] + target_version = self.condition_data[index][1] + user_version = self.attributes.get(condition_name) + + if not isinstance(target_version, string_types): + self.logger.warning(audience_logs.UNKNOWN_CONDITION_VALUE.format(self._get_condition_json(index), )) + return None + + if not isinstance(user_version, string_types): + self.logger.warning( + audience_logs.UNEXPECTED_TYPE.format( + self._get_condition_json(index), type(user_version), condition_name + ) + ) + return None + + result = self.compare_user_version_with_target_version(target_version, user_version) + if result is None: + return None + + return result < 0 + + def semver_less_than_or_equal_evaluator(self, index): + """ Evaluate the given semantic version less than or equal to match target version for the user version. + + Args: + index: Index of the condition to be evaluated. + + Returns: + Boolean: + - True if the user version is less than or equal to the target version. + - False if the user version is greater than the target version. + None: + - if the user version value is not string type or is null. + """ + condition_name = self.condition_data[index][0] + target_version = self.condition_data[index][1] + user_version = self.attributes.get(condition_name) + + if not isinstance(target_version, string_types): + self.logger.warning(audience_logs.UNKNOWN_CONDITION_VALUE.format(self._get_condition_json(index), )) + return None + + if not isinstance(user_version, string_types): + self.logger.warning( + audience_logs.UNEXPECTED_TYPE.format( + self._get_condition_json(index), type(user_version), condition_name + ) + ) + return None + + result = self.compare_user_version_with_target_version(target_version, user_version) + if result is None: + return None + + return result <= 0 + + def semver_greater_than_or_equal_evaluator(self, index): + """ Evaluate the given semantic version greater than or equal to match target version for the user version. + + Args: + index: Index of the condition to be evaluated. + + Returns: + Boolean: + - True if the user version is greater than or equal to the target version. + - False if the user version is less than the target version. + None: + - if the user version value is not string type or is null. + """ + condition_name = self.condition_data[index][0] + target_version = self.condition_data[index][1] + user_version = self.attributes.get(condition_name) + + if not isinstance(target_version, string_types): + self.logger.warning(audience_logs.UNKNOWN_CONDITION_VALUE.format(self._get_condition_json(index), )) + return None + + if not isinstance(user_version, string_types): + self.logger.warning( + audience_logs.UNEXPECTED_TYPE.format( + self._get_condition_json(index), type(user_version), condition_name + ) + ) + return None + + result = self.compare_user_version_with_target_version(target_version, user_version) + if result is None: + return None + + return result >= 0 + EVALUATORS_BY_MATCH_TYPE = { ConditionMatchTypes.EXACT: exact_evaluator, ConditionMatchTypes.EXISTS: exists_evaluator, ConditionMatchTypes.GREATER_THAN: greater_than_evaluator, + ConditionMatchTypes.GREATER_THAN_OR_EQUAL: greater_than_or_equal_evaluator, ConditionMatchTypes.LESS_THAN: less_than_evaluator, - ConditionMatchTypes.SUBSTRING: substring_evaluator, + ConditionMatchTypes.LESS_THAN_OR_EQUAL: less_than_or_equal_evaluator, + ConditionMatchTypes.SEMVER_EQ: semver_equal_evaluator, + ConditionMatchTypes.SEMVER_GE: semver_greater_than_or_equal_evaluator, + ConditionMatchTypes.SEMVER_GT: semver_greater_than_evaluator, + ConditionMatchTypes.SEMVER_LE: semver_less_than_or_equal_evaluator, + ConditionMatchTypes.SEMVER_LT: semver_less_than_evaluator, + ConditionMatchTypes.SUBSTRING: substring_evaluator } + def split_version(self, version): + """ Method to split the given version. + + Args: + version: Given version. + + Returns: + List: + - The array of version split into smaller parts i.e major, minor, patch etc + None: + - if the given version is invalid in format + """ + target_prefix = version + target_suffix = "" + target_parts = [] + + # check that version shouldn't have white space + if self.has_white_space(version): + self.logger.warning(Errors.INVALID_ATTRIBUTE_FORMAT) + return None + + # check for pre release e.g. 1.0.0-alpha where 'alpha' is a pre release + # otherwise check for build e.g. 1.0.0+001 where 001 is a build metadata + if self.is_pre_release_version(version) or self.is_build_version(version): + target_parts = version.split(VersionType.IS_PRE_RELEASE, 1) if self.is_pre_release_version(version) else \ + version.split(VersionType.IS_BUILD, 1) + + # split version into prefix and suffix + if target_parts: + if len(target_parts) < 1: + self.logger.warning(Errors.INVALID_ATTRIBUTE_FORMAT) + return None + target_prefix = str(target_parts[0]) + target_suffix = target_parts[1:] + + # check dot counts in target_prefix + dot_count = target_prefix.count(".") + if dot_count > 2: + self.logger.warning(Errors.INVALID_ATTRIBUTE_FORMAT) + return None + + target_version_parts = target_prefix.split(".") + if len(target_version_parts) != dot_count + 1: + self.logger.warning(Errors.INVALID_ATTRIBUTE_FORMAT) + return None + for part in target_version_parts: + if not part.isdigit(): + self.logger.warning(Errors.INVALID_ATTRIBUTE_FORMAT) + return None + + if target_suffix: + target_version_parts.extend(target_suffix) + return target_version_parts + def evaluate(self, index): """ Given a custom attribute audience condition and user attributes, evaluate the condition against the attributes. diff --git a/optimizely/helpers/enums.py b/optimizely/helpers/enums.py index beaba157..3eed4a30 100644 --- a/optimizely/helpers/enums.py +++ b/optimizely/helpers/enums.py @@ -157,3 +157,8 @@ class NotificationTypes(object): OPTIMIZELY_CONFIG_UPDATE = 'OPTIMIZELY_CONFIG_UPDATE' TRACK = 'TRACK:event_key, user_id, attributes, event_tags, event' LOG_EVENT = 'LOG_EVENT:log_event' + + +class VersionType(object): + IS_PRE_RELEASE = '-' + IS_BUILD = '+' diff --git a/tests/base.py b/tests/base.py index 432d5287..9dceec2d 100644 --- a/tests/base.py +++ b/tests/base.py @@ -518,6 +518,7 @@ def setUp(self, config_dict='config_dict'): '3468206647', '3468206644', '3468206643', + '18278344267' ], 'variations': [ {'variables': [], 'id': '11557362669', 'key': '11557362669', 'featureEnabled': True} @@ -556,7 +557,8 @@ def setUp(self, config_dict='config_dict'): 'audienceConditions': [ 'and', ['or', '3468206642', '3988293898'], - ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643'], + ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643', + '18278344267'], ], 'variations': [ {'variables': [], 'id': '11557362670', 'key': '11557362670', 'featureEnabled': True} @@ -626,6 +628,7 @@ def setUp(self, config_dict='config_dict'): '3468206647', '3468206644', '3468206643', + '18278344267' ], 'variations': [ { @@ -653,6 +656,7 @@ def setUp(self, config_dict='config_dict'): '3468206647', '3468206644', '3468206643', + '18278344267' ], 'forcedVariations': {}, }, @@ -667,7 +671,7 @@ def setUp(self, config_dict='config_dict'): 'audienceConditions': [ 'and', ['or', '3468206642', '3988293898'], - ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643'], + ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643', '18278344267'], ], 'forcedVariations': {}, }, @@ -689,7 +693,7 @@ def setUp(self, config_dict='config_dict'): 'audienceConditions': [ 'and', ['or', '3468206642', '3988293898'], - ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643'], + ['or', '3988293899', '3468206646', '3468206647', '3468206644', '3468206643', '18278344267'], ], 'forcedVariations': {}, }, @@ -837,6 +841,37 @@ def setUp(self, config_dict='config_dict'): ], ], }, + { + "id": "18278344267", + "name": "semverReleaseLt1.2.3Gt1.0.0", + "conditions": [ + "and", + [ + "or", + [ + "or", + { + "value": "1.2.3", + "type": "custom_attribute", + "name": "android-release", + "match": "semver_lt" + } + ] + ], + [ + "or", + [ + "or", + { + "value": "1.0.0", + "type": "custom_attribute", + "name": "android-release", + "match": "semver_gt" + } + ] + ] + ] + } ], 'groups': [], 'attributes': [ @@ -844,6 +879,7 @@ def setUp(self, config_dict='config_dict'): {'key': 'lasers', 'id': '594016'}, {'key': 'should_do_it', 'id': '594017'}, {'key': 'favorite_ice_cream', 'id': '594018'}, + {'key': 'android-release', 'id': '594019'}, ], 'botFiltering': False, 'accountId': '4879520872', diff --git a/tests/helpers_tests/test_condition.py b/tests/helpers_tests/test_condition.py index b4dee368..1a20e9ae 100644 --- a/tests/helpers_tests/test_condition.py +++ b/tests/helpers_tests/test_condition.py @@ -1,4 +1,4 @@ -# Copyright 2016-2019, Optimizely +# Copyright 2016-2020, Optimizely # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at @@ -32,11 +32,15 @@ substring_condition_list = [['headline_text', 'buy now', 'custom_attribute', 'substring']] gt_int_condition_list = [['meters_travelled', 48, 'custom_attribute', 'gt']] gt_float_condition_list = [['meters_travelled', 48.2, 'custom_attribute', 'gt']] +ge_int_condition_list = [['meters_travelled', 48, 'custom_attribute', 'ge']] +ge_float_condition_list = [['meters_travelled', 48.2, 'custom_attribute', 'ge']] lt_int_condition_list = [['meters_travelled', 48, 'custom_attribute', 'lt']] lt_float_condition_list = [['meters_travelled', 48.2, 'custom_attribute', 'lt']] +le_int_condition_list = [['meters_travelled', 48, 'custom_attribute', 'le']] +le_float_condition_list = [['meters_travelled', 48.2, 'custom_attribute', 'le']] -class CustomAttributeConditionEvaluator(base.BaseTest): +class CustomAttributeConditionEvaluatorTest(base.BaseTest): def setUp(self): base.BaseTest.setUp(self) self.condition_list = [ @@ -108,6 +112,208 @@ def test_evaluate__returns_null__when_condition_has_an_invalid_type_property(sel self.assertIsNone(evaluator.evaluate(0)) + def test_semver_eq__returns_true(self): + semver_equal_2_0_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_eq']] + user_versions = ['2.0.0', '2.0'] + for user_version in user_versions: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + semver_equal_2_0_condition_list, {'Android': user_version}, self.mock_client_logger) + result = evaluator.evaluate(0) + custom_err_msg = "Got {} in result. Failed for user version: {}".format(result, user_version) + self.assertTrue(result, custom_err_msg) + + def test_semver_eq__returns_false(self): + semver_equal_2_0_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_eq']] + user_versions = ['2.9', '1.9'] + for user_version in user_versions: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + semver_equal_2_0_condition_list, {'Android': user_version}, self.mock_client_logger) + result = evaluator.evaluate(0) + custom_err_msg = "Got {} in result. Failed for user version: {}".format(result, user_version) + self.assertFalse(result, custom_err_msg) + + def test_semver_le__returns_true(self): + semver_less_than_or_equal_2_0_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_le']] + user_versions = ['2.0.0', '1.9'] + for user_version in user_versions: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + semver_less_than_or_equal_2_0_condition_list, {'Android': user_version}, self.mock_client_logger) + result = evaluator.evaluate(0) + custom_err_msg = "Got {} in result. Failed for user version: {}".format(result, user_version) + self.assertTrue(result, custom_err_msg) + + def test_semver_le__returns_false(self): + semver_less_than_or_equal_2_0_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_le']] + user_versions = ['2.5.1'] + for user_version in user_versions: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + semver_less_than_or_equal_2_0_condition_list, {'Android': user_version}, self.mock_client_logger) + result = evaluator.evaluate(0) + custom_err_msg = "Got {} in result. Failed for user version: {}".format(result, user_version) + self.assertFalse(result, custom_err_msg) + + def test_semver_ge__returns_true(self): + semver_greater_than_or_equal_2_0_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_ge']] + user_versions = ['2.0.0', '2.9'] + for user_version in user_versions: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + semver_greater_than_or_equal_2_0_condition_list, {'Android': user_version}, self.mock_client_logger) + result = evaluator.evaluate(0) + custom_err_msg = "Got {} in result. Failed for user version: {}".format(result, user_version) + self.assertTrue(result, custom_err_msg) + + def test_semver_ge__returns_false(self): + semver_greater_than_or_equal_2_0_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_ge']] + user_versions = ['1.9'] + for user_version in user_versions: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + semver_greater_than_or_equal_2_0_condition_list, {'Android': user_version}, self.mock_client_logger) + result = evaluator.evaluate(0) + custom_err_msg = "Got {} in result. Failed for user version: {}".format(result, user_version) + self.assertFalse(result, custom_err_msg) + + def test_semver_lt__returns_true(self): + semver_less_than_2_0_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_lt']] + user_versions = ['1.9'] + for user_version in user_versions: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + semver_less_than_2_0_condition_list, {'Android': user_version}, self.mock_client_logger) + result = evaluator.evaluate(0) + custom_err_msg = "Got {} in result. Failed for user version: {}".format(result, user_version) + self.assertTrue(result, custom_err_msg) + + def test_semver_lt__returns_false(self): + semver_less_than_2_0_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_lt']] + user_versions = ['2.0.0', '2.5.1'] + for user_version in user_versions: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + semver_less_than_2_0_condition_list, {'Android': user_version}, self.mock_client_logger) + result = evaluator.evaluate(0) + custom_err_msg = "Got {} in result. Failed for user version: {}".format(result, user_version) + self.assertFalse(result, custom_err_msg) + + def test_semver_gt__returns_true(self): + semver_greater_than_2_0_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_gt']] + user_versions = ['2.9'] + for user_version in user_versions: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + semver_greater_than_2_0_condition_list, {'Android': user_version}, self.mock_client_logger) + result = evaluator.evaluate(0) + custom_err_msg = "Got {} in result. Failed for user version: {}".format(result, user_version) + self.assertTrue(result, custom_err_msg) + + def test_semver_gt__returns_false(self): + semver_greater_than_2_0_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_gt']] + user_versions = ['2.0.0', '1.9'] + for user_version in user_versions: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + semver_greater_than_2_0_condition_list, {'Android': user_version}, self.mock_client_logger) + result = evaluator.evaluate(0) + custom_err_msg = "Got {} in result. Failed for user version: {}".format(result, user_version) + self.assertFalse(result, custom_err_msg) + + def test_evaluate__returns_None__when_user_version_is_not_string(self): + semver_greater_than_2_0_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_gt']] + user_versions = [True, 37] + for user_version in user_versions: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + semver_greater_than_2_0_condition_list, {'Android': user_version}, self.mock_client_logger) + result = evaluator.evaluate(0) + custom_err_msg = "Got {} in result. Failed for user version: {}".format(result, user_version) + self.assertIsNone(result, custom_err_msg) + + def test_evaluate__returns_None__when_user_version_with_invalid_semantic(self): + semver_greater_than_2_0_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_gt']] + user_versions = ['3.7.2.2', '+'] + for user_version in user_versions: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + semver_greater_than_2_0_condition_list, {'Android': user_version}, self.mock_client_logger) + result = evaluator.evaluate(0) + custom_err_msg = "Got {} in result. Failed for user version: {}".format(result, user_version) + self.assertIsNone(result, custom_err_msg) + + def test_compare_user_version_with_target_version_equal_to_0(self): + semver_greater_than_2_0_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_gt']] + versions = [ + ('2.0.1', '2.0.1'), + ('2.9.9-beta', '2.9.9-beta'), + ('2.1', '2.1.0'), + ('2', '2.12'), + ('2.9', '2.9.1'), + ('2.9.1', '2.9.1+beta') + ] + for target_version, user_version in versions: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + semver_greater_than_2_0_condition_list, {'Android': user_version}, self.mock_client_logger) + result = evaluator.compare_user_version_with_target_version(target_version, user_version) + custom_err_msg = "Got {} in result. Failed for user version:" \ + " {} and target version: {}".format(result, + user_version, + target_version + ) + self.assertEqual(result, 0, custom_err_msg) + + def test_compare_user_version_with_target_version_greater_than_0(self): + semver_greater_than_2_0_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_gt']] + versions = [ + ('2.0.0', '2.0.1'), + ('2.0', '3.0.1'), + ('2.1.2-beta', '2.1.2-release'), + ('2.1.3-beta1', '2.1.3-beta2'), + ('2.9.9-beta', '2.9.9'), + ('2.9.9+beta', '2.9.9'), + ('3.7.0-prerelease+build', '3.7.0-prerelease+rc'), + ('2.2.3-beta-beta1', '2.2.3-beta-beta2'), + ('2.2.3-beta+beta1', '2.2.3-beta+beta2'), + ('2.2.3+beta2-beta1', '2.2.3+beta3-beta2') + ] + for target_version, user_version in versions: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + semver_greater_than_2_0_condition_list, {'Android': user_version}, self.mock_client_logger) + result = evaluator.compare_user_version_with_target_version(target_version, user_version) + custom_err_msg = "Got {} in result. Failed for user version:" \ + " {} and target version: {}".format(result, + user_version, + target_version) + self.assertEqual(result, 1, custom_err_msg) + + def test_compare_user_version_with_target_version_less_than_0(self): + semver_greater_than_2_0_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_gt']] + versions = [ + ('2.0.1', '2.0.0'), + ('3.0', '2.0.1'), + ('2.3', '2.0.1'), + ('2.3.5', '2.3.1'), + ('2.9.8', '2.9'), + ('2.1.2-release', '2.1.2-beta'), + ('2.9.9+beta', '2.9.9-beta'), + ('3.7.0+build3.7.0-prerelease+build', '3.7.0-prerelease'), + ('2.1.3-beta-beta2', '2.1.3-beta'), + ('2.1.3-beta1+beta3', '2.1.3-beta1+beta2') + ] + for target_version, user_version in versions: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + semver_greater_than_2_0_condition_list, {'Android': user_version}, self.mock_client_logger) + result = evaluator.compare_user_version_with_target_version(target_version, user_version) + custom_err_msg = "Got {} in result. Failed for user version: {} " \ + "and target version: {}".format(result, + user_version, + target_version) + self.assertEqual(result, -1, custom_err_msg) + + def test_compare_invalid_user_version_with(self): + semver_greater_than_2_0_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_gt']] + versions = ['-', '.', '..', '+', '+test', ' ', '2 .3. 0', '2.', '.2.2', '3.7.2.2', '3.x', ',', + '+build-prerelease', '2..2'] + target_version = '2.1.0' + + for user_version in versions: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + semver_greater_than_2_0_condition_list, {'Android': user_version}, self.mock_client_logger) + result = evaluator.compare_user_version_with_target_version(user_version, target_version) + custom_err_msg = "Got {} in result. Failed for user version: {}".format(result, user_version) + self.assertIsNone(result, custom_err_msg) + def test_exists__returns_false__when_no_user_provided_value(self): evaluator = condition_helper.CustomAttributeConditionEvaluator( @@ -154,7 +360,7 @@ def test_exists__returns_true__when_user_provided_value_is_boolean(self): 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, @@ -162,7 +368,7 @@ def test_exact_string__returns_true__when_user_provided_value_is_equal_to_condit 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, @@ -170,7 +376,7 @@ def test_exact_string__returns_false__when_user_provided_value_is_not_equal_to_c 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, @@ -186,7 +392,7 @@ def test_exact_string__returns_null__when_no_user_provided_value(self): 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( @@ -207,7 +413,7 @@ def test_exact_int__returns_true__when_user_provided_value_is_equal_to_condition 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( @@ -228,7 +434,7 @@ def test_exact_float__returns_true__when_user_provided_value_is_equal_to_conditi 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 @@ -236,7 +442,7 @@ def test_exact_int__returns_false__when_user_provided_value_is_not_equal_to_cond 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, @@ -244,7 +450,7 @@ def test_exact_float__returns_false__when_user_provided_value_is_not_equal_to_co 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 @@ -258,7 +464,7 @@ def test_exact_int__returns_null__when_user_provided_value_is_different_type_fro 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 @@ -315,7 +521,7 @@ def test_exact__given_number_values__calls_is_finite_number(self): 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, @@ -323,7 +529,7 @@ def test_exact_bool__returns_true__when_user_provided_value_is_equal_to_conditio 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, @@ -331,7 +537,7 @@ def test_exact_bool__returns_false__when_user_provided_value_is_not_equal_to_con 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 @@ -347,7 +553,7 @@ def test_exact_bool__returns_null__when_no_user_provided_value(self): 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, @@ -355,7 +561,7 @@ def test_substring__returns_true__when_condition_value_is_substring_of_user_valu 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, @@ -379,7 +585,7 @@ def test_substring__returns_null__when_no_user_provided_value(self): 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 @@ -400,7 +606,7 @@ def test_greater_than_int__returns_true__when_user_value_greater_than_condition_ 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 @@ -421,7 +627,7 @@ def test_greater_than_float__returns_true__when_user_value_greater_than_conditio 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 @@ -442,7 +648,7 @@ def test_greater_than_int__returns_false__when_user_value_not_greater_than_condi 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 @@ -507,7 +713,149 @@ def test_greater_than_float__returns_null__when_no_user_provided_value(self): self.assertIsNone(evaluator.evaluate(0)) - def test_less_than_int__returns_true__when_user_value_less_than_condition_value(self,): + def test_greater_than_or_equal_int__returns_true__when_user_value_greater_than_or_equal_condition_value(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_int_condition_list, {'meters_travelled': 48.1}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_int_condition_list, {'meters_travelled': 48}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_int_condition_list, {'meters_travelled': 49}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + if PY2: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_int_condition_list, {'meters_travelled': long(49)}, self.mock_client_logger, + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + def test_greater_than_or_equal_float__returns_true__when_user_value_greater_than_or_equal_condition_value(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_float_condition_list, {'meters_travelled': 48.3}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_float_condition_list, {'meters_travelled': 48.2}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_float_condition_list, {'meters_travelled': 49}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + if PY2: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_float_condition_list, {'meters_travelled': long(49)}, self.mock_client_logger, + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + def test_greater_than_or_equal_int__returns_false__when_user_value_not_greater_than_or_equal_condition_value( + self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_int_condition_list, {'meters_travelled': 47.9}, self.mock_client_logger + ) + + self.assertStrictFalse(evaluator.evaluate(0)) + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_int_condition_list, {'meters_travelled': 47}, self.mock_client_logger + ) + + self.assertStrictFalse(evaluator.evaluate(0)) + + if PY2: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_int_condition_list, {'meters_travelled': long(47)}, self.mock_client_logger, + ) + + self.assertStrictFalse(evaluator.evaluate(0)) + + def test_greater_than_or_equal_float__returns_false__when_user_value_not_greater_than_or_equal_condition_value( + self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_float_condition_list, {'meters_travelled': 48.1}, self.mock_client_logger + ) + + self.assertStrictFalse(evaluator.evaluate(0)) + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_float_condition_list, {'meters_travelled': 48}, self.mock_client_logger + ) + + self.assertStrictFalse(evaluator.evaluate(0)) + + if PY2: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_float_condition_list, {'meters_travelled': long(48)}, self.mock_client_logger, + ) + + self.assertStrictFalse(evaluator.evaluate(0)) + + def test_greater_than_or_equal_int__returns_null__when_user_value_is_not_a_number(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_int_condition_list, {'meters_travelled': 'a long way'}, self.mock_client_logger, + ) + + self.assertIsNone(evaluator.evaluate(0)) + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_int_condition_list, {'meters_travelled': False}, self.mock_client_logger + ) + + self.assertIsNone(evaluator.evaluate(0)) + + def test_greater_than_or_equal_float__returns_null__when_user_value_is_not_a_number(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_float_condition_list, {'meters_travelled': 'a long way'}, self.mock_client_logger, + ) + + self.assertIsNone(evaluator.evaluate(0)) + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_float_condition_list, {'meters_travelled': False}, self.mock_client_logger, + ) + + self.assertIsNone(evaluator.evaluate(0)) + + def test_greater_than_or_equal_int__returns_null__when_no_user_provided_value(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_int_condition_list, {}, self.mock_client_logger + ) + + self.assertIsNone(evaluator.evaluate(0)) + + def test_greater_than_or_equal_float__returns_null__when_no_user_provided_value(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_float_condition_list, {}, self.mock_client_logger + ) + + self.assertIsNone(evaluator.evaluate(0)) + + 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 @@ -528,7 +876,7 @@ def test_less_than_int__returns_true__when_user_value_less_than_condition_value( 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 @@ -549,7 +897,7 @@ def test_less_than_float__returns_true__when_user_value_less_than_condition_valu 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 @@ -570,7 +918,7 @@ def test_less_than_int__returns_false__when_user_value_not_less_than_condition_v 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 @@ -623,6 +971,140 @@ def test_less_than_float__returns_null__when_no_user_provided_value(self): self.assertIsNone(evaluator.evaluate(0)) + def test_less_than_or_equal_int__returns_true__when_user_value_less_than_or_equal_condition_value(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_int_condition_list, {'meters_travelled': 47.9}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_int_condition_list, {'meters_travelled': 47}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_int_condition_list, {'meters_travelled': 48}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + if PY2: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_int_condition_list, {'meters_travelled': long(47)}, self.mock_client_logger, + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_int_condition_list, {'meters_travelled': long(48)}, self.mock_client_logger, + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + def test_less_than_or_equal_float__returns_true__when_user_value_less_than_or_equal_condition_value(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_float_condition_list, {'meters_travelled': 48.1}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_float_condition_list, {'meters_travelled': 48.2}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_float_condition_list, {'meters_travelled': 48}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + if PY2: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_float_condition_list, {'meters_travelled': long(48)}, self.mock_client_logger, + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + def test_less_than_or_equal_int__returns_false__when_user_value_not_less_than_or_equal_condition_value(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_int_condition_list, {'meters_travelled': 48.1}, self.mock_client_logger + ) + + self.assertStrictFalse(evaluator.evaluate(0)) + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_int_condition_list, {'meters_travelled': 49}, self.mock_client_logger + ) + + self.assertStrictFalse(evaluator.evaluate(0)) + + if PY2: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_int_condition_list, {'meters_travelled': long(49)}, self.mock_client_logger, + ) + + self.assertStrictFalse(evaluator.evaluate(0)) + + def test_less_than_or_equal_float__returns_false__when_user_value_not_less_than_or_equal_condition_value(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_float_condition_list, {'meters_travelled': 48.3}, self.mock_client_logger + ) + + self.assertStrictFalse(evaluator.evaluate(0)) + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_float_condition_list, {'meters_travelled': 49}, self.mock_client_logger + ) + + self.assertStrictFalse(evaluator.evaluate(0)) + + if PY2: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_float_condition_list, {'meters_travelled': long(49)}, self.mock_client_logger, + ) + + self.assertStrictFalse(evaluator.evaluate(0)) + + def test_less_than_or_equal_int__returns_null__when_user_value_is_not_a_number(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_int_condition_list, {'meters_travelled': False}, self.mock_client_logger + ) + + self.assertIsNone(evaluator.evaluate(0)) + + def test_less_than_or_equal_float__returns_null__when_user_value_is_not_a_number(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_float_condition_list, {'meters_travelled': False}, self.mock_client_logger, + ) + + self.assertIsNone(evaluator.evaluate(0)) + + def test_less_than_or_equal_int__returns_null__when_no_user_provided_value(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_int_condition_list, {}, self.mock_client_logger + ) + + self.assertIsNone(evaluator.evaluate(0)) + + def test_less_than_or_equal_float__returns_null__when_no_user_provided_value(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_float_condition_list, {}, self.mock_client_logger + ) + + self.assertIsNone(evaluator.evaluate(0)) + 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. """ @@ -637,7 +1119,8 @@ def is_finite_number__rejecting_condition_value(value): return True with mock.patch( - 'optimizely.helpers.validator.is_finite_number', side_effect=is_finite_number__rejecting_condition_value, + 'optimizely.helpers.validator.is_finite_number', + side_effect=is_finite_number__rejecting_condition_value, ) as mock_is_finite: self.assertIsNone(evaluator.evaluate(0)) @@ -650,8 +1133,8 @@ def is_finite_number__rejecting_user_attribute_value(value): return True with mock.patch( - 'optimizely.helpers.validator.is_finite_number', - side_effect=is_finite_number__rejecting_user_attribute_value, + 'optimizely.helpers.validator.is_finite_number', + side_effect=is_finite_number__rejecting_user_attribute_value, ) as mock_is_finite: self.assertIsNone(evaluator.evaluate(0)) @@ -662,7 +1145,7 @@ 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, + 'optimizely.helpers.validator.is_finite_number', side_effect=is_finite_number__accepting_both_values, ): self.assertTrue(evaluator.evaluate(0)) @@ -680,7 +1163,8 @@ def is_finite_number__rejecting_condition_value(value): return True with mock.patch( - 'optimizely.helpers.validator.is_finite_number', side_effect=is_finite_number__rejecting_condition_value, + 'optimizely.helpers.validator.is_finite_number', + side_effect=is_finite_number__rejecting_condition_value, ) as mock_is_finite: self.assertIsNone(evaluator.evaluate(0)) @@ -693,8 +1177,8 @@ def is_finite_number__rejecting_user_attribute_value(value): return True with mock.patch( - 'optimizely.helpers.validator.is_finite_number', - side_effect=is_finite_number__rejecting_user_attribute_value, + 'optimizely.helpers.validator.is_finite_number', + side_effect=is_finite_number__rejecting_user_attribute_value, ) as mock_is_finite: self.assertIsNone(evaluator.evaluate(0)) @@ -705,10 +1189,112 @@ 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, + 'optimizely.helpers.validator.is_finite_number', side_effect=is_finite_number__accepting_both_values, + ): + self.assertTrue(evaluator.evaluate(0)) + + def test_greater_than_or_equal__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( + ge_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 + + 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) + + 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)) + + # 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 + + 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_or_equal__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( + le_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 + + 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) + + 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)) + + # 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 + + with mock.patch( + 'optimizely.helpers.validator.is_finite_number', side_effect=is_finite_number__accepting_both_values, + ): + self.assertTrue(evaluator.evaluate(0)) + + def test_invalid_semver__returns_None__when_semver_is_invalid(self): + semver_less_than_or_equal_2_0_1_condition_list = [['Android', "2.0.1", 'custom_attribute', 'semver_le']] + invalid_test_cases = ["-", ".", "..", "+", "+test", " ", "2 .0. 0", + "2.", ".0.0", "1.2.2.2", "2.x", ",", + "+build-prerelease", "2..0"] + + for user_version in invalid_test_cases: + evaluator = condition_helper.CustomAttributeConditionEvaluator( + semver_less_than_or_equal_2_0_1_condition_list, {'Android': user_version}, self.mock_client_logger) + + result = evaluator.evaluate(0) + custom_err_msg = "Got {} in result. Failed for user version: {}".format(result, user_version) + self.assertIsNone(result, custom_err_msg) + class ConditionDecoderTests(base.BaseTest): def test_loads(self): diff --git a/tests/test_optimizely.py b/tests/test_optimizely.py index 94783a7a..f586c44c 100644 --- a/tests/test_optimizely.py +++ b/tests/test_optimizely.py @@ -844,6 +844,48 @@ def test_activate__with_attributes__typed_audience_match(self): 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_with_semver_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 '18278344267' + self.assertEqual( + 'A', opt_obj.activate('typed_audience_experiment', 'test_user', {'android-release': '1.0.1'}), + ) + expected_attr = { + 'type': 'custom', + 'value': '1.0.1', + 'entity_id': '594019', + 'key': 'android-release', + } + + 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: + self.assertEqual( + 'A', opt_obj.activate('typed_audience_experiment', 'test_user', {'android-release': "1.2.2"}), + ) + expected_attr = { + 'type': 'custom', + 'value': "1.2.2", + 'entity_id': '594019', + 'key': 'android-release', + } + + 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_with_semver_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', {'android-release': '1.2.9'})) + self.assertEqual(0, mock_process.call_count) + 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))