diff --git a/optimizely/helpers/condition.py b/optimizely/helpers/condition.py index 15a22ef1..fecc47d4 100644 --- a/optimizely/helpers/condition.py +++ b/optimizely/helpers/condition.py @@ -31,7 +31,9 @@ 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' @@ -177,6 +179,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. @@ -211,6 +247,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. @@ -331,7 +401,8 @@ def split_semantic_version(self, target): target_parts = [] if self.has_white_space(target): - raise Exception(Errors.INVALID_ATTRIBUTE_FORMAT) + self.logger.warning(Errors.INVALID_ATTRIBUTE_FORMAT) + return None if self.is_pre_release(target): target_parts = target.split(SemverType.IS_PRE_RELEASE) @@ -340,20 +411,24 @@ def split_semantic_version(self, target): if target_parts: if len(target_parts) < 1: - raise Exception(Errors.INVALID_ATTRIBUTE_FORMAT) + self.logger.warning(Errors.INVALID_ATTRIBUTE_FORMAT) + return None target_prefix = str(target_parts[0]) target_suffix = target_parts[1:] dot_count = target_prefix.count(".") if dot_count > 2: - raise Exception(Errors.INVALID_ATTRIBUTE_FORMAT) + self.logger.warning(Errors.INVALID_ATTRIBUTE_FORMAT) + return None target_version_parts = target_prefix.split(".") if len(target_version_parts) != dot_count + 1: - raise Exception(Errors.INVALID_ATTRIBUTE_FORMAT) + self.logger.warning(Errors.INVALID_ATTRIBUTE_FORMAT) + return None for part in target_version_parts: if not part.isdigit(): - raise Exception(Errors.INVALID_ATTRIBUTE_FORMAT) + self.logger.warning(Errors.INVALID_ATTRIBUTE_FORMAT) + return None if target_suffix: target_version_parts.extend(target_suffix) @@ -453,7 +528,9 @@ def compare_user_version_with_target_version(self, index): 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 + ConditionMatchTypes.SUBSTRING: substring_evaluator, + ConditionMatchTypes.LESS_THAN_OR_EQUAL: less_than_or_equal_evaluator, + ConditionMatchTypes.GREATER_THAN_OR_EQUAL: greater_than_or_equal_evaluator, } def evaluate(self, index): diff --git a/tests/helpers_tests/test_condition.py b/tests/helpers_tests/test_condition.py index 68b7beef..b74bce65 100644 --- a/tests/helpers_tests/test_condition.py +++ b/tests/helpers_tests/test_condition.py @@ -46,8 +46,12 @@ 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): @@ -788,6 +792,145 @@ def test_greater_than_float__returns_null__when_no_user_provided_value(self): self.assertIsNone(evaluator.evaluate(0)) + 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(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( @@ -904,6 +1047,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. """ @@ -990,6 +1267,92 @@ def is_finite_number__accepting_both_values(value): ): 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)) + class ConditionDecoderTests(base.BaseTest): def test_loads(self):