From 686d6a3a1bba3ac0f9d7c7cdd4cd0427a8cff99b Mon Sep 17 00:00:00 2001 From: Amna Ejaz Date: Mon, 13 Jul 2020 17:15:25 +0500 Subject: [PATCH 1/7] semver added - wip --- optimizely/helpers/condition.py | 91 +++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/optimizely/helpers/condition.py b/optimizely/helpers/condition.py index 0676aecb..11f04544 100644 --- a/optimizely/helpers/condition.py +++ b/optimizely/helpers/condition.py @@ -32,6 +32,11 @@ class ConditionMatchTypes(object): GREATER_THAN = 'gt' LESS_THAN = 'lt' SUBSTRING = 'substring' + SEMVER_EQ = 'semver_eq' + SEMVER_GT = 'semver_gt' + SEMVER_LT = 'semver_lt' + SEMVER_GE = 'semver_ge' + SEMVER_LE = 'semver_le' class CustomAttributeConditionEvaluator(object): @@ -233,12 +238,98 @@ def substring_evaluator(self, index): return condition_value in user_value + def semver_equal_evaluator(self, index): + + 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): + 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 + + return condition_value == user_value + + def semver_greater_than_evaluator(self, index): + + condition_name = self.condition_data[index][0] + condition_value = self.condition_data[index][1] + user_value = self.attributes.get(condition_name) + + condition_version_parts = condition_value.split(".") + user_version_parts = user_value.split(".") + + condition_version_parts_len = len(condition_version_parts) + user_version_parts_len = len(user_version_parts) + + # fill smaller version with 0s + if condition_version_parts_len > user_version_parts_len: + for i in range(user_version_parts_len, condition_version_parts_len): + user_version_parts.append("0") + elif user_version_parts_len > condition_version_parts_len: + for i in range(condition_version_parts_len, user_version_parts_len): + condition_version_parts.append("0") + + for (idx, _) in enumerate(condition_version_parts): + if int(user_version_parts[idx]) > int(condition_version_parts[idx]): + return True + else: + return False + + def semver_less_than_evaluator(self, index): + + condition_name = self.condition_data[index][0] + condition_value = self.condition_data[index][1] + user_value = self.attributes.get(condition_name) + + condition_version_parts = condition_value.split(".") + user_version_parts = user_value.split(".") + + condition_version_parts_len = len(condition_version_parts) + user_version_parts_len = len(user_version_parts) + + # fill smaller version with 0s + if condition_version_parts_len > user_version_parts_len: + for i in range(user_version_parts_len, condition_version_parts_len): + user_version_parts.append("0") + elif user_version_parts_len > condition_version_parts_len: + for i in range(condition_version_parts_len, user_version_parts_len): + condition_version_parts.append("0") + + for (idx, _) in enumerate(condition_version_parts): + if int(user_version_parts[idx]) < int(condition_version_parts[idx]): + return True + else: + return False + + def semver_less_than_and_equal_evaluator(self, index): + if self.semver_less_than_evaluator(self, index) or self.semver_equal_evaluator(self, index): + return True + + def semver_greater_than_and_equal_evaluator(self, index): + if self.semver_greater_than_evaluator(self, index) or self.semver_equal_evaluator(self, index): + return True + + 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, + ConditionMatchTypes.SEMVER_EQ: exact_evaluator, + ConditionMatchTypes.SEMVER_GT: semver_greater_than_evaluator, + ConditionMatchTypes.SEMVER_LT: semver_less_than_evaluator, + ConditionMatchTypes.SEMVER_LE: semver_less_than_and_equal_evaluator, + ConditionMatchTypes.SEMVER_GE: semver_greater_than_and_equal_evaluator } def evaluate(self, index): From 89e68e0564fccf8c3cd38ddeeae4c56d0f545640 Mon Sep 17 00:00:00 2001 From: Amna Ejaz Date: Tue, 14 Jul 2020 11:25:57 +0500 Subject: [PATCH 2/7] added utility fn to compare functions --- optimizely/helpers/condition.py | 72 +++++++++++++-------------------- 1 file changed, 29 insertions(+), 43 deletions(-) diff --git a/optimizely/helpers/condition.py b/optimizely/helpers/condition.py index 11f04544..8b7a41dd 100644 --- a/optimizely/helpers/condition.py +++ b/optimizely/helpers/condition.py @@ -244,19 +244,10 @@ def semver_equal_evaluator(self, index): 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): - 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 - - return condition_value == user_value + if self.compare_versions(condition_value, user_value) is None: + return True + else: + return False def semver_greater_than_evaluator(self, index): @@ -264,25 +255,10 @@ def semver_greater_than_evaluator(self, index): condition_value = self.condition_data[index][1] user_value = self.attributes.get(condition_name) - condition_version_parts = condition_value.split(".") - user_version_parts = user_value.split(".") - - condition_version_parts_len = len(condition_version_parts) - user_version_parts_len = len(user_version_parts) - - # fill smaller version with 0s - if condition_version_parts_len > user_version_parts_len: - for i in range(user_version_parts_len, condition_version_parts_len): - user_version_parts.append("0") - elif user_version_parts_len > condition_version_parts_len: - for i in range(condition_version_parts_len, user_version_parts_len): - condition_version_parts.append("0") - - for (idx, _) in enumerate(condition_version_parts): - if int(user_version_parts[idx]) > int(condition_version_parts[idx]): - return True - else: - return False + if self.compare_versions(condition_value, user_value) is True: + return True + else: + return False def semver_less_than_evaluator(self, index): @@ -290,8 +266,23 @@ def semver_less_than_evaluator(self, index): condition_value = self.condition_data[index][1] user_value = self.attributes.get(condition_name) - condition_version_parts = condition_value.split(".") - user_version_parts = user_value.split(".") + if self.compare_versions(condition_value, user_value) is False: + return True + else: + return False + + def semver_less_than_and_equal_evaluator(self, index): + if self.semver_less_than_evaluator(self, index) is True or self.semver_equal_evaluator(self, index) is True: + return True + + def semver_greater_than_and_equal_evaluator(self, index): + if self.semver_greater_than_evaluator(self, index) is True or self.semver_equal_evaluator(self, index) is True: + return True + + def compare_versions(self, target_version, user_version): + + condition_version_parts = target_version.split(".") + user_version_parts = user_version.split(".") condition_version_parts_len = len(condition_version_parts) user_version_parts_len = len(user_version_parts) @@ -304,19 +295,14 @@ def semver_less_than_evaluator(self, index): for i in range(condition_version_parts_len, user_version_parts_len): condition_version_parts.append("0") + # returns True if Greater, False if smaller and None if equal for (idx, _) in enumerate(condition_version_parts): - if int(user_version_parts[idx]) < int(condition_version_parts[idx]): + if int(user_version_parts[idx]) > int(condition_version_parts[idx]): return True - else: + elif int(user_version_parts[idx]) < int(condition_version_parts[idx]): return False + return None - def semver_less_than_and_equal_evaluator(self, index): - if self.semver_less_than_evaluator(self, index) or self.semver_equal_evaluator(self, index): - return True - - def semver_greater_than_and_equal_evaluator(self, index): - if self.semver_greater_than_evaluator(self, index) or self.semver_equal_evaluator(self, index): - return True EVALUATORS_BY_MATCH_TYPE = { From 80627b91a5fb2bae140c8a243fb75803091510da Mon Sep 17 00:00:00 2001 From: Amna Ejaz Date: Tue, 14 Jul 2020 13:38:43 +0500 Subject: [PATCH 3/7] unit tests added for semver --- optimizely/helpers/condition.py | 19 +++-- tests/helpers_tests/test_condition.py | 103 +++++++++++++++++++++++++- 2 files changed, 113 insertions(+), 9 deletions(-) diff --git a/optimizely/helpers/condition.py b/optimizely/helpers/condition.py index 8b7a41dd..841aba2e 100644 --- a/optimizely/helpers/condition.py +++ b/optimizely/helpers/condition.py @@ -244,7 +244,7 @@ def semver_equal_evaluator(self, index): condition_value = self.condition_data[index][1] user_value = self.attributes.get(condition_name) - if self.compare_versions(condition_value, user_value) is None: + if self.compare_user_version_with_target_version(user_value, condition_value) is None: return True else: return False @@ -255,7 +255,7 @@ def semver_greater_than_evaluator(self, index): condition_value = self.condition_data[index][1] user_value = self.attributes.get(condition_name) - if self.compare_versions(condition_value, user_value) is True: + if self.compare_user_version_with_target_version(user_value, condition_value) is True: return True else: return False @@ -266,20 +266,24 @@ def semver_less_than_evaluator(self, index): condition_value = self.condition_data[index][1] user_value = self.attributes.get(condition_name) - if self.compare_versions(condition_value, user_value) is False: + if self.compare_user_version_with_target_version(user_value, condition_value) is False: return True else: return False def semver_less_than_and_equal_evaluator(self, index): - if self.semver_less_than_evaluator(self, index) is True or self.semver_equal_evaluator(self, index) is True: + if self.semver_less_than_evaluator(index) is True or self.semver_equal_evaluator(index) is True: return True + else: + return False def semver_greater_than_and_equal_evaluator(self, index): - if self.semver_greater_than_evaluator(self, index) is True or self.semver_equal_evaluator(self, index) is True: + if self.semver_greater_than_evaluator(index) is True or self.semver_equal_evaluator(index) is True: return True + else: + return False - def compare_versions(self, target_version, user_version): + def compare_user_version_with_target_version(self, user_version, target_version): condition_version_parts = target_version.split(".") user_version_parts = user_version.split(".") @@ -287,6 +291,7 @@ def compare_versions(self, target_version, user_version): condition_version_parts_len = len(condition_version_parts) user_version_parts_len = len(user_version_parts) + # fill smaller version with 0s if condition_version_parts_len > user_version_parts_len: for i in range(user_version_parts_len, condition_version_parts_len): @@ -311,7 +316,7 @@ def compare_versions(self, target_version, user_version): ConditionMatchTypes.GREATER_THAN: greater_than_evaluator, ConditionMatchTypes.LESS_THAN: less_than_evaluator, ConditionMatchTypes.SUBSTRING: substring_evaluator, - ConditionMatchTypes.SEMVER_EQ: exact_evaluator, + ConditionMatchTypes.SEMVER_EQ: semver_equal_evaluator, ConditionMatchTypes.SEMVER_GT: semver_greater_than_evaluator, ConditionMatchTypes.SEMVER_LT: semver_less_than_evaluator, ConditionMatchTypes.SEMVER_LE: semver_less_than_and_equal_evaluator, diff --git a/tests/helpers_tests/test_condition.py b/tests/helpers_tests/test_condition.py index b4dee368..6c946433 100644 --- a/tests/helpers_tests/test_condition.py +++ b/tests/helpers_tests/test_condition.py @@ -24,6 +24,12 @@ integerCondition = ['num_users', 10, 'custom_attribute', 'exact'] doubleCondition = ['pi_value', 3.14, 'custom_attribute', 'exact'] +eq_semver_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_eq']] +lt_semver_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_lt']] +gt_semver_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_gt']] +le_semver_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_le']] +ge_semver_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_ge']] + exists_condition_list = [['input_value', None, 'custom_attribute', 'exists']] exact_string_condition_list = [['favorite_constellation', 'Lacerta', 'custom_attribute', 'exact']] exact_int_condition_list = [['lasers_count', 9000, 'custom_attribute', 'exact']] @@ -36,6 +42,8 @@ lt_float_condition_list = [['meters_travelled', 48.2, 'custom_attribute', 'lt']] + + class CustomAttributeConditionEvaluator(base.BaseTest): def setUp(self): base.BaseTest.setUp(self) @@ -108,6 +116,98 @@ def test_evaluate__returns_null__when_condition_has_an_invalid_type_property(sel self.assertIsNone(evaluator.evaluate(0)) + def test_evaluate__returns_true__when_audience_version_matches_condition_version(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + eq_semver_condition_list, {'Android': '2.0.0'}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + def test_evaluate__returns_true__when_audience_version_doesn_not_match_condition_version(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + eq_semver_condition_list, {'Android': '2.1.1'}, self.mock_client_logger + ) + + self.assertStrictFalse(evaluator.evaluate(0)) + + def test_evaluate__returns_true__when_audience_version_is_gt_condition_version(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_semver_condition_list, {'Android': '2.1.1'}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + def test_evaluate__returns_true__when_audience_version_is_not_gt_condition_version(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + gt_semver_condition_list, {'Android': '1.1.1'}, self.mock_client_logger + ) + + self.assertStrictFalse(evaluator.evaluate(0)) + + def test_evaluate__returns_true__when_audience_version_is_lt_condition_version(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_semver_condition_list, {'Android': '1.9.1'}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + def test_evaluate__returns_true__when_audience_version_is_not_lt_condition_version(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + lt_semver_condition_list, {'Android': '2.9.1'}, self.mock_client_logger + ) + + self.assertStrictFalse(evaluator.evaluate(0)) + + def test_evaluate__returns_true__when_audience_version_is_ge_condition_version(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_semver_condition_list, {'Android': '2.0.0'}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_semver_condition_list, {'Android': '2.0.7'}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + def test_evaluate__returns_true__when_audience_version_is_not_ge_condition_version(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_semver_condition_list, {'Android': '1.0.0'}, self.mock_client_logger + ) + + self.assertStrictFalse(evaluator.evaluate(0)) + + def test_evaluate__returns_true__when_audience_version_is_le_condition_version(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_semver_condition_list, {'Android': '2.0'}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_semver_condition_list, {'Android': '1.9.9'}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + def test_evaluate__returns_true__when_audience_version_is_not_le_condition_version(self): + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_semver_condition_list, {'Android': '3.0.1'}, self.mock_client_logger + ) + + self.assertStrictFalse(evaluator.evaluate(0)) + def test_exists__returns_false__when_no_user_provided_value(self): evaluator = condition_helper.CustomAttributeConditionEvaluator( @@ -154,8 +254,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_version_is_equal_to_condition_value(self, ): evaluator = condition_helper.CustomAttributeConditionEvaluator( exact_string_condition_list, {'favorite_constellation': 'Lacerta'}, self.mock_client_logger, ) From f9aec0a91ad23dbea7c1b9eafe831d6b0e4f5560 Mon Sep 17 00:00:00 2001 From: Amna Ejaz Date: Tue, 14 Jul 2020 13:45:04 +0500 Subject: [PATCH 4/7] reverted unecessary changes --- tests/helpers_tests/test_condition.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/helpers_tests/test_condition.py b/tests/helpers_tests/test_condition.py index 6c946433..09cce173 100644 --- a/tests/helpers_tests/test_condition.py +++ b/tests/helpers_tests/test_condition.py @@ -42,8 +42,6 @@ lt_float_condition_list = [['meters_travelled', 48.2, 'custom_attribute', 'lt']] - - class CustomAttributeConditionEvaluator(base.BaseTest): def setUp(self): base.BaseTest.setUp(self) @@ -254,7 +252,8 @@ 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_version_is_equal_to_condition_value(self, ): + def test_exact_string__returns_true__when_user_provided_version_is_equal_to_condition_value(self,): + evaluator = condition_helper.CustomAttributeConditionEvaluator( exact_string_condition_list, {'favorite_constellation': 'Lacerta'}, self.mock_client_logger, ) From 24b6088db7bcd881e80c7f4f9d41e10f211b7d15 Mon Sep 17 00:00:00 2001 From: Amna Ejaz Date: Tue, 14 Jul 2020 13:48:46 +0500 Subject: [PATCH 5/7] Update test_condition.py --- tests/helpers_tests/test_condition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/helpers_tests/test_condition.py b/tests/helpers_tests/test_condition.py index 09cce173..058ea05e 100644 --- a/tests/helpers_tests/test_condition.py +++ b/tests/helpers_tests/test_condition.py @@ -252,7 +252,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_version_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, From 379cb19a1ee0fab54a56fec5dd2485f0644914ac Mon Sep 17 00:00:00 2001 From: Amna Ejaz Date: Tue, 14 Jul 2020 16:36:11 +0500 Subject: [PATCH 6/7] ran flake8 --- optimizely/helpers/condition.py | 3 --- tests/helpers_tests/test_condition.py | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/optimizely/helpers/condition.py b/optimizely/helpers/condition.py index 841aba2e..c69e4553 100644 --- a/optimizely/helpers/condition.py +++ b/optimizely/helpers/condition.py @@ -291,7 +291,6 @@ def compare_user_version_with_target_version(self, user_version, target_version) condition_version_parts_len = len(condition_version_parts) user_version_parts_len = len(user_version_parts) - # fill smaller version with 0s if condition_version_parts_len > user_version_parts_len: for i in range(user_version_parts_len, condition_version_parts_len): @@ -308,8 +307,6 @@ def compare_user_version_with_target_version(self, user_version, target_version) return False return None - - EVALUATORS_BY_MATCH_TYPE = { ConditionMatchTypes.EXACT: exact_evaluator, ConditionMatchTypes.EXISTS: exists_evaluator, diff --git a/tests/helpers_tests/test_condition.py b/tests/helpers_tests/test_condition.py index 058ea05e..2739f71a 100644 --- a/tests/helpers_tests/test_condition.py +++ b/tests/helpers_tests/test_condition.py @@ -199,7 +199,7 @@ def test_evaluate__returns_true__when_audience_version_is_le_condition_version(s self.assertStrictTrue(evaluator.evaluate(0)) def test_evaluate__returns_true__when_audience_version_is_not_le_condition_version(self): - + evaluator = condition_helper.CustomAttributeConditionEvaluator( le_semver_condition_list, {'Android': '3.0.1'}, self.mock_client_logger ) From fb1cbc0641592c52f4d18aa5a682e83de2c9d04a Mon Sep 17 00:00:00 2001 From: Amna Ejaz Date: Wed, 15 Jul 2020 16:56:08 +0500 Subject: [PATCH 7/7] updated compare fn, added more unit tests also added semver_audience with its test --- optimizely/helpers/condition.py | 39 ++++++----------- tests/base.py | 42 ++++++++++++++++-- tests/helpers_tests/test_condition.py | 62 +++++++++++++++++++++------ tests/test_optimizely.py | 42 ++++++++++++++++++ 4 files changed, 144 insertions(+), 41 deletions(-) diff --git a/optimizely/helpers/condition.py b/optimizely/helpers/condition.py index c69e4553..713d7ca6 100644 --- a/optimizely/helpers/condition.py +++ b/optimizely/helpers/condition.py @@ -244,10 +244,7 @@ def semver_equal_evaluator(self, index): condition_value = self.condition_data[index][1] user_value = self.attributes.get(condition_name) - if self.compare_user_version_with_target_version(user_value, condition_value) is None: - return True - else: - return False + return self.compare_user_version_with_target_version(user_value, condition_value) is 0 def semver_greater_than_evaluator(self, index): @@ -255,10 +252,7 @@ def semver_greater_than_evaluator(self, index): condition_value = self.condition_data[index][1] user_value = self.attributes.get(condition_name) - if self.compare_user_version_with_target_version(user_value, condition_value) is True: - return True - else: - return False + return self.compare_user_version_with_target_version(user_value, condition_value) is 1 def semver_less_than_evaluator(self, index): @@ -266,22 +260,13 @@ def semver_less_than_evaluator(self, index): condition_value = self.condition_data[index][1] user_value = self.attributes.get(condition_name) - if self.compare_user_version_with_target_version(user_value, condition_value) is False: - return True - else: - return False + return self.compare_user_version_with_target_version(user_value, condition_value) is -1 def semver_less_than_and_equal_evaluator(self, index): - if self.semver_less_than_evaluator(index) is True or self.semver_equal_evaluator(index) is True: - return True - else: - return False + return self.semver_less_than_evaluator(index) is True or self.semver_equal_evaluator(index) is True def semver_greater_than_and_equal_evaluator(self, index): - if self.semver_greater_than_evaluator(index) is True or self.semver_equal_evaluator(index) is True: - return True - else: - return False + return self.semver_greater_than_evaluator(index) is True or self.semver_equal_evaluator(index) is True def compare_user_version_with_target_version(self, user_version, target_version): @@ -299,13 +284,17 @@ def compare_user_version_with_target_version(self, user_version, target_version) for i in range(condition_version_parts_len, user_version_parts_len): condition_version_parts.append("0") - # returns True if Greater, False if smaller and None if equal for (idx, _) in enumerate(condition_version_parts): - if int(user_version_parts[idx]) > int(condition_version_parts[idx]): - return True + # compare strings e.g: n1.n2.n3-alpha/beta + if not user_version_parts[idx].isnumeric(): + if user_version_parts[idx] != condition_version_parts[idx]: + return -1 + # compare numbers e.g: n1.n2.n3 + elif int(user_version_parts[idx]) > int(condition_version_parts[idx]): + return 1 elif int(user_version_parts[idx]) < int(condition_version_parts[idx]): - return False - return None + return -1 + return 0 EVALUATORS_BY_MATCH_TYPE = { ConditionMatchTypes.EXACT: exact_evaluator, diff --git a/tests/base.py b/tests/base.py index 432d5287..0e3f3995 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,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'], ], 'variations': [ {'variables': [], 'id': '11557362670', 'key': '11557362670', 'featureEnabled': True} @@ -626,6 +627,7 @@ def setUp(self, config_dict='config_dict'): '3468206647', '3468206644', '3468206643', + '18278344267' ], 'variations': [ { @@ -653,6 +655,7 @@ def setUp(self, config_dict='config_dict'): '3468206647', '3468206644', '3468206643', + '18278344267' ], 'forcedVariations': {}, }, @@ -667,7 +670,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 +692,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 +840,37 @@ def setUp(self, config_dict='config_dict'): ], ], }, + { + "id": "18278344267", + "name": "semver_audience", + "conditions": [ + "and", + [ + "or", + [ + "or", + { + "value": "1.2.3", + "type": "custom_attribute", + "name": "Android", + "match": "semver_lt" + } + ] + ], + [ + "or", + [ + "or", + { + "value": "1.0.0", + "type": "custom_attribute", + "name": "Android", + "match": "semver_gt" + } + ] + ] + ] + } ], 'groups': [], 'attributes': [ @@ -844,6 +878,8 @@ 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', 'id': '594019'}, + ], 'botFiltering': False, 'accountId': '4879520872', diff --git a/tests/helpers_tests/test_condition.py b/tests/helpers_tests/test_condition.py index 2739f71a..2261f803 100644 --- a/tests/helpers_tests/test_condition.py +++ b/tests/helpers_tests/test_condition.py @@ -24,11 +24,17 @@ integerCondition = ['num_users', 10, 'custom_attribute', 'exact'] doubleCondition = ['pi_value', 3.14, 'custom_attribute', 'exact'] -eq_semver_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_eq']] +eq_semver_condition_list_variation_1 = [['Android', "2.0", 'custom_attribute', 'semver_eq']] +eq_semver_condition_list_variation_2 = [['Android', "2.0.1-beta.0", 'custom_attribute', 'semver_eq']] +eq_semver_condition_list_variation_3 = [['Android', "2.0.0", 'custom_attribute', 'semver_eq']] +eq_semver_condition_list_variation_4 = [['Android', "2.0.1-beta", 'custom_attribute', 'semver_eq']] + lt_semver_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_lt']] -gt_semver_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_gt']] -le_semver_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_le']] -ge_semver_condition_list = [['Android', "2.0", 'custom_attribute', 'semver_ge']] +gt_semver_condition_list = [['Android', "2.0.0", 'custom_attribute', 'semver_gt']] +ge_semver_condition_list_variation_1 = [['Android', "2.0.9", 'custom_attribute', 'semver_ge']] +ge_semver_condition_list_variation_2 = [['Android', "2.0.9-beta", 'custom_attribute', 'semver_ge']] +le_semver_condition_list_variation_1 = [['Android', "2.0.1", 'custom_attribute', 'semver_le']] +le_semver_condition_list_variation_2 = [['Android', "2.0.1-apha", 'custom_attribute', 'semver_le']] exists_condition_list = [['input_value', None, 'custom_attribute', 'exists']] exact_string_condition_list = [['favorite_constellation', 'Lacerta', 'custom_attribute', 'exact']] @@ -117,15 +123,33 @@ def test_evaluate__returns_null__when_condition_has_an_invalid_type_property(sel def test_evaluate__returns_true__when_audience_version_matches_condition_version(self): evaluator = condition_helper.CustomAttributeConditionEvaluator( - eq_semver_condition_list, {'Android': '2.0.0'}, self.mock_client_logger + eq_semver_condition_list_variation_1, {'Android': '2.0.0'}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + eq_semver_condition_list_variation_2, {'Android': '2.0.1-beta'}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + eq_semver_condition_list_variation_3, {'Android': '2.0'}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + eq_semver_condition_list_variation_4, {'Android': '2.0.1-beta.0.0'}, self.mock_client_logger ) self.assertStrictTrue(evaluator.evaluate(0)) - def test_evaluate__returns_true__when_audience_version_doesn_not_match_condition_version(self): + def test_evaluate__returns_true__when_audience_version_does_not_match_condition_version(self): evaluator = condition_helper.CustomAttributeConditionEvaluator( - eq_semver_condition_list, {'Android': '2.1.1'}, self.mock_client_logger + eq_semver_condition_list_variation_1, {'Android': '2.1.1'}, self.mock_client_logger ) self.assertStrictFalse(evaluator.evaluate(0)) @@ -165,13 +189,19 @@ def test_evaluate__returns_true__when_audience_version_is_not_lt_condition_versi def test_evaluate__returns_true__when_audience_version_is_ge_condition_version(self): evaluator = condition_helper.CustomAttributeConditionEvaluator( - ge_semver_condition_list, {'Android': '2.0.0'}, self.mock_client_logger + ge_semver_condition_list_variation_1, {'Android': '2.0.9'}, self.mock_client_logger ) self.assertStrictTrue(evaluator.evaluate(0)) evaluator = condition_helper.CustomAttributeConditionEvaluator( - ge_semver_condition_list, {'Android': '2.0.7'}, self.mock_client_logger + ge_semver_condition_list_variation_1, {'Android': '2.9'}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + ge_semver_condition_list_variation_1, {'Android': '2.0.9-beta'}, self.mock_client_logger ) self.assertStrictTrue(evaluator.evaluate(0)) @@ -179,7 +209,7 @@ def test_evaluate__returns_true__when_audience_version_is_ge_condition_version(s def test_evaluate__returns_true__when_audience_version_is_not_ge_condition_version(self): evaluator = condition_helper.CustomAttributeConditionEvaluator( - ge_semver_condition_list, {'Android': '1.0.0'}, self.mock_client_logger + ge_semver_condition_list_variation_1, {'Android': '1.0.0'}, self.mock_client_logger ) self.assertStrictFalse(evaluator.evaluate(0)) @@ -187,13 +217,19 @@ def test_evaluate__returns_true__when_audience_version_is_not_ge_condition_versi def test_evaluate__returns_true__when_audience_version_is_le_condition_version(self): evaluator = condition_helper.CustomAttributeConditionEvaluator( - le_semver_condition_list, {'Android': '2.0'}, self.mock_client_logger + le_semver_condition_list_variation_1, {'Android': '2.0.1'}, self.mock_client_logger + ) + + self.assertStrictTrue(evaluator.evaluate(0)) + + evaluator = condition_helper.CustomAttributeConditionEvaluator( + le_semver_condition_list_variation_1, {'Android': '1.1'}, self.mock_client_logger ) self.assertStrictTrue(evaluator.evaluate(0)) evaluator = condition_helper.CustomAttributeConditionEvaluator( - le_semver_condition_list, {'Android': '1.9.9'}, self.mock_client_logger + le_semver_condition_list_variation_2, {'Android': '2.0.1-beta'}, self.mock_client_logger ) self.assertStrictTrue(evaluator.evaluate(0)) @@ -201,7 +237,7 @@ def test_evaluate__returns_true__when_audience_version_is_le_condition_version(s def test_evaluate__returns_true__when_audience_version_is_not_le_condition_version(self): evaluator = condition_helper.CustomAttributeConditionEvaluator( - le_semver_condition_list, {'Android': '3.0.1'}, self.mock_client_logger + le_semver_condition_list_variation_1, {'Android': '3.0.1'}, self.mock_client_logger ) self.assertStrictFalse(evaluator.evaluate(0)) diff --git a/tests/test_optimizely.py b/tests/test_optimizely.py index 94783a7a..713ac2fe 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': '1.0.1'}), + ) + expected_attr = { + 'type': 'custom', + 'value': '1.0.1', + 'entity_id': '594019', + 'key': 'Android', + } + + 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': "1.2.2"}), + ) + expected_attr = { + 'type': 'custom', + 'value': "1.2.2", + 'entity_id': '594019', + 'key': 'Android', + } + + 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': '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))