From 051b181d18f7774ea5d826c1b85ed6c7e30ffba6 Mon Sep 17 00:00:00 2001 From: Akash Date: Thu, 12 Dec 2024 17:33:19 +0530 Subject: [PATCH] feat: Implement VersionCompare evaluator for version string comparisons Added Version Compare evaluator --- src/tirith/core/core.py | 5 +- src/tirith/core/evaluators/__init__.py | 2 + src/tirith/core/evaluators/version_compare.py | 79 +++++++++++++++++++ tests/core/evaluators/test_version_compare.py | 45 +++++++++++ 4 files changed, 130 insertions(+), 1 deletion(-) create mode 100644 src/tirith/core/evaluators/version_compare.py create mode 100644 tests/core/evaluators/test_version_compare.py diff --git a/src/tirith/core/core.py b/src/tirith/core/core.py index 9cb9437..c9112ef 100644 --- a/src/tirith/core/core.py +++ b/src/tirith/core/core.py @@ -33,7 +33,10 @@ def generate_evaluator_result(evaluator_obj, input_data, provider_module): provider_inputs = evaluator_obj.get("provider_args") condition = evaluator_obj.get("condition") evaluator_name: str = condition.get("type") - evaluator_data = condition.get("value") + if evaluator_name is "VersionCompare": + evaluator_data = { "value": condition.get("value"), "operation": condition.get("operation") } + else: + evaluator_data = condition.get("value") evaluator_error_tolerance: int = condition.get("error_tolerance", DEFAULT_ERROR_TOLERANCE) if not condition: diff --git a/src/tirith/core/evaluators/__init__.py b/src/tirith/core/evaluators/__init__.py index 5666af0..ce9ca28 100644 --- a/src/tirith/core/evaluators/__init__.py +++ b/src/tirith/core/evaluators/__init__.py @@ -14,6 +14,7 @@ from .not_equals import NotEquals from .not_contained_in import NotContainedIn from .not_contains import NotContains +from .version_compare import VersionCompare EVALUATORS_DICT: Dict[str, Type[BaseEvaluator]] = { "ContainedIn": ContainedIn, @@ -29,4 +30,5 @@ "NotEquals": NotEquals, "NotContainedIn": NotContainedIn, "NotContains": NotContains, + "VersionCompare": VersionCompare } diff --git a/src/tirith/core/evaluators/version_compare.py b/src/tirith/core/evaluators/version_compare.py new file mode 100644 index 0000000..e88ff43 --- /dev/null +++ b/src/tirith/core/evaluators/version_compare.py @@ -0,0 +1,79 @@ +from .base_evaluator import BaseEvaluator +from packaging.version import Version +import re + + + # Compares two version strings based on the provided operation. + + # Args: + # evaluator_input (str): The first version string to compare. + # evaluator_data (str): The second version string to compare. + # operation (str): The comparison operation to perform. + # Supported values: "LessThan", "LessThanOrEquals", "Equals", "GreaterThan", "GreaterThanOrEquals". + + # Returns: + # dict: A dictionary containing the comparison result (`passed`) and a descriptive message. + + # Example: + # + # >>> comparator = VersionCompare() + # >>> comparator.evaluate("1.0.0", {"value": "1.0.1", "operation": "lessThan"}) + # {'passed': True, 'message': '1.0.0 is less than 1.0.1'} + # >>> comparator.evaluate("1.0.0", {"value": "1.0.0", "operation": "equal"}) + # {'passed': True, 'message': '1.0.0 is equal to 1.0.0'} + # >>> comparator.evaluate("2.0.0", {"value": "1.0.0", "operation": "greaterThan"}) + # {'passed': True, 'message': '2.0.0 is greater than 1.0.0'} + + + # .. versionadded:: 1.0.0 + + + +class VersionCompare(BaseEvaluator): + """Compares two version strings based on the provided operation.""" + + def parse_version(self, version_string): + """Parses a version string and returns a comparable version object.""" + # Extract the version part from the string + match = re.search(r'[\d]+(\.[\d]+)*(\-[a-zA-Z0-9]+)?$', version_string) + if match: + try: + return Version(match.group(0)) + except Exception as e: + raise ValueError(f"Invalid version format: {version_string}. Error: {str(e)}") + else: + raise ValueError(f"Invalid version format: {version_string}") + + def evaluate(self, evaluator_input, evaluator_data): + """Compares two version strings based on the provided operation.""" + evaluation_result = {"passed": False, "message": "Not evaluated"} + try: + v1 = self.parse_version(evaluator_input) + v2 = self.parse_version(evaluator_data['value']) + operation = evaluator_data['operation'] + + if operation == "LessThan": + evaluation_result["passed"] = v1 < v2 + elif operation == "LessThanOrEquals": + evaluation_result["passed"] = v1 <= v2 + elif operation == "Equals": + evaluation_result["passed"] = v1 == v2 + elif operation == "GreaterThan": + evaluation_result["passed"] = v1 > v2 + elif operation == "GreaterThanOrEquals": + evaluation_result["passed"] = v1 >= v2 + + else: + raise ValueError(f"Unsupported operation: {operation}") + + if evaluation_result["passed"]: + evaluation_result["message"] = f"{evaluator_input} is {operation.replace('_', ' ')} {evaluator_data}" + else: + evaluation_result["message"] = f"{evaluator_input} is not {operation.replace('_', ' ')} {evaluator_data}" + + return evaluation_result + + except ValueError as e: + evaluation_result["message"] = str(e) + return evaluation_result + diff --git a/tests/core/evaluators/test_version_compare.py b/tests/core/evaluators/test_version_compare.py new file mode 100644 index 0000000..8ee5f0b --- /dev/null +++ b/tests/core/evaluators/test_version_compare.py @@ -0,0 +1,45 @@ +from tirith.core.evaluators import VersionCompare +from pytest import mark + +# Define test cases +checks_passing = [ + ("1.0.0", {"value": "1.0.1", "operation": "LessThan"}), + ("1.0.0", {"value": "1.0.0", "operation": "Equals"}), + ("2.0.0", {"value": "1.0.0", "operation": "GreaterThan"}), + ("1.0.0", {"value": "1.1.0", "operation": "LessThanOrEquals"}), + ("1.1.0", {"value": "1.1.0", "operation": "LessThanOrEquals"}), + ("1.2.0", {"value": "1.1.0", "operation": "GreaterThanOrEquals"}), + ("1.1.0", {"value": "1.1.0", "operation": "GreaterThanOrEquals"}), +] + +checks_failing = [ + ("1.0.0", {"value": "1.0.1", "operation": "GreaterThan"}), + ("1.0.0", {"value": "1.0.0", "operation": "LessThan"}), + ("1.0.1", {"value": "1.0.0", "operation": "LessThan"}), + ("1.1.0", {"value": "1.0.0", "operation": "Equals"}), +] + + +evaluator = VersionCompare() + +# pytest -v -m passing +@mark.passing +@mark.parametrize("evaluator_input,evaluator_data", checks_passing) +def test_evaluate_passing(evaluator_input, evaluator_data): + result = evaluator.evaluate(evaluator_input, evaluator_data) + operation = evaluator_data["operation"] + assert result == { + "passed": True, + "message": f"{evaluator_input} is {operation} {evaluator_data['value']}" + } + +# pytest -v -m failing +@mark.failing +@mark.parametrize("evaluator_input,evaluator_data", checks_failing) +def test_evaluate_failing(evaluator_input, evaluator_data): + result = evaluator.evaluate(evaluator_input, evaluator_data) + operation = evaluator_data["operation"] + assert result == { + "passed": False, + "message": f"{evaluator_input} is not {operation} {evaluator_data['value']}" + } \ No newline at end of file