diff --git a/src/data_factory_testing_framework/models/activities/_activity.py b/src/data_factory_testing_framework/models/activities/_activity.py index cc72dfbf..a61e1b58 100644 --- a/src/data_factory_testing_framework/models/activities/_activity.py +++ b/src/data_factory_testing_framework/models/activities/_activity.py @@ -86,7 +86,7 @@ def _evaluate_expressions( if not attribute.startswith("_") and not callable(getattr(obj, attribute)) ] for attribute_name in attribute_names: - if "activities" in attribute_name: + if self._is_nested_activities_name(attribute_name): continue attribute = getattr(obj, attribute_name) @@ -98,7 +98,7 @@ def _evaluate_expressions( # Dictionary if isinstance(obj, dict): for key in obj.keys(): - if "activities" in key: + if self._is_nested_activities_name(key): continue self._evaluate_expressions(obj[key], state, visited, types_to_ignore) @@ -116,6 +116,12 @@ def _evaluate_expressions( self._evaluate_expressions(item, state, visited, types_to_ignore) + @staticmethod + def _is_nested_activities_name(name: str) -> bool: + # Nested activities (activities, ifTrueActivities, ifFalseActivities, defaultActivities, ...) + # are evaluated by the control activity in their own scope, never eagerly with the parent. + return "activities" in name.lower() + def set_result(self, result: DependencyCondition, output: Optional[Any] = None) -> None: # noqa: ANN401 self.status = result self.output = output diff --git a/tests/functional/branch_not_taken/pipeline/if_condition_branch.json b/tests/functional/branch_not_taken/pipeline/if_condition_branch.json new file mode 100644 index 00000000..fd23d520 --- /dev/null +++ b/tests/functional/branch_not_taken/pipeline/if_condition_branch.json @@ -0,0 +1,61 @@ +{ + "name": "if_condition_branch", + "properties": { + "activities": [ + { + "name": "Has_Date", + "type": "IfCondition", + "dependsOn": [], + "userProperties": [], + "typeProperties": { + "expression": { + "value": "@empty(pipeline().parameters.date)", + "type": "Expression" + }, + "ifTrueActivities": [ + { + "name": "Set_Today", + "type": "SetVariable", + "dependsOn": [], + "userProperties": [], + "typeProperties": { + "variableName": "day", + "value": { + "value": "@formatDateTime(utcNow(), 'yyyy-MM-dd')", + "type": "Expression" + } + } + } + ], + "ifFalseActivities": [ + { + "name": "Set_Given_Date", + "type": "SetVariable", + "dependsOn": [], + "userProperties": [], + "typeProperties": { + "variableName": "day", + "value": { + "value": "@formatDateTime(pipeline().parameters.date, 'yyyy-MM-dd')", + "type": "Expression" + } + } + } + ] + } + } + ], + "parameters": { + "date": { + "type": "string", + "defaultValue": "" + } + }, + "variables": { + "day": { + "type": "String" + } + }, + "annotations": [] + } +} diff --git a/tests/functional/branch_not_taken/pipeline/switch_default_branch.json b/tests/functional/branch_not_taken/pipeline/switch_default_branch.json new file mode 100644 index 00000000..f2620a6d --- /dev/null +++ b/tests/functional/branch_not_taken/pipeline/switch_default_branch.json @@ -0,0 +1,70 @@ +{ + "name": "switch_default_branch", + "properties": { + "activities": [ + { + "name": "On_Mode", + "type": "Switch", + "dependsOn": [], + "userProperties": [], + "typeProperties": { + "on": { + "value": "@pipeline().parameters.mode", + "type": "Expression" + }, + "cases": [ + { + "value": "today", + "activities": [ + { + "name": "Set_Today", + "type": "SetVariable", + "dependsOn": [], + "userProperties": [], + "typeProperties": { + "variableName": "day", + "value": { + "value": "@formatDateTime(utcNow(), 'yyyy-MM-dd')", + "type": "Expression" + } + } + } + ] + } + ], + "defaultActivities": [ + { + "name": "Set_Given_Date", + "type": "SetVariable", + "dependsOn": [], + "userProperties": [], + "typeProperties": { + "variableName": "day", + "value": { + "value": "@formatDateTime(pipeline().parameters.date, 'yyyy-MM-dd')", + "type": "Expression" + } + } + } + ] + } + } + ], + "parameters": { + "mode": { + "type": "string", + "defaultValue": "today" + }, + "date": { + "type": "string", + "defaultValue": "" + } + }, + "variables": { + "day": { + "type": "String" + } + }, + "annotations": [] + } +} diff --git a/tests/functional/branch_not_taken/test_branch_not_taken.py b/tests/functional/branch_not_taken/test_branch_not_taken.py new file mode 100644 index 00000000..5fd85013 --- /dev/null +++ b/tests/functional/branch_not_taken/test_branch_not_taken.py @@ -0,0 +1,78 @@ +import pytest +from data_factory_testing_framework import TestFramework, TestFrameworkType +from data_factory_testing_framework.state import RunParameter, RunParameterType + + +@pytest.mark.parametrize( + "date,expected_activity_name,expected_day", + [ + ("", "Set_Today", None), + ("2024-01-31T00:00:00Z", "Set_Given_Date", "2024-01-31"), + ], +) +def test_if_condition_only_evaluates_the_branch_taken( + date: str, + expected_activity_name: str, + expected_day: str, + request: pytest.FixtureRequest, +) -> None: + # Arrange + test_framework = TestFramework( + framework_type=TestFrameworkType.DataFactory, + root_folder_path=request.fspath.dirname, + ) + pipeline = test_framework.get_pipeline_by_name("if_condition_branch") + + # Act + activities = test_framework.evaluate_pipeline( + pipeline, + [ + RunParameter(RunParameterType.Pipeline, "date", date), + ], + ) + + # Assert + activity = next(activities) + assert activity.name == expected_activity_name + if expected_day is not None: + assert activity.type_properties["value"].result == expected_day + + with pytest.raises(StopIteration): + next(activities) + + +@pytest.mark.parametrize( + "mode,date,expected_activity_name", + [ + ("today", "", "Set_Today"), + ("given", "2024-01-31T00:00:00Z", "Set_Given_Date"), + ], +) +def test_switch_only_evaluates_the_case_taken( + mode: str, + date: str, + expected_activity_name: str, + request: pytest.FixtureRequest, +) -> None: + # Arrange + test_framework = TestFramework( + framework_type=TestFrameworkType.DataFactory, + root_folder_path=request.fspath.dirname, + ) + pipeline = test_framework.get_pipeline_by_name("switch_default_branch") + + # Act + activities = test_framework.evaluate_pipeline( + pipeline, + [ + RunParameter(RunParameterType.Pipeline, "mode", mode), + RunParameter(RunParameterType.Pipeline, "date", date), + ], + ) + + # Assert + activity = next(activities) + assert activity.name == expected_activity_name + + with pytest.raises(StopIteration): + next(activities) diff --git a/tests/unit/models/activities/base/test_activity.py b/tests/unit/models/activities/base/test_activity.py index 9eb080f6..5c951735 100644 --- a/tests/unit/models/activities/base/test_activity.py +++ b/tests/unit/models/activities/base/test_activity.py @@ -105,3 +105,40 @@ def test_evaluate_is_evaluating_expressions_inside_dict() -> None: # Assert assert pipeline_activity.type_properties["parameters"]["url"].result == "example.com" + + +@pytest.mark.parametrize( + "nested_activities_key", + ["activities", "ifTrueActivities", "ifFalseActivities", "defaultActivities"], +) +def test_evaluate_does_not_evaluate_expressions_of_nested_activities(nested_activities_key: str) -> None: + # Arrange + activity = Activity( + name="activity", + type="Container", + typeProperties={ + "url": DataFactoryElement("@pipeline().parameters.url"), + nested_activities_key: [ + { + "name": "nested", + "type": "SetVariable", + "typeProperties": { + "variableName": "variable", + "value": DataFactoryElement("@pipeline().parameters.not_in_scope"), + }, + }, + ], + }, + ) + state = PipelineRunState( + parameters=[ + RunParameter(RunParameterType.Pipeline, "url", "example.com"), + ], + ) + + # Act + activity.evaluate(state) + + # Assert + assert activity.type_properties["url"].result == "example.com" + assert activity.type_properties[nested_activities_key][0]["typeProperties"]["value"].result is None