Example #1
0
    def assert_workflow_with_single_item(self, ac_ex_status, tk_ex_status, wf_ex_status):
        wf_def = """
        version: 1.0

        vars:
          - xs:
              - fee

        tasks:
          task1:
            with: <% ctx(xs) %>
            action: core.echo message=<% item() %>
        """

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # Mock the action execution for each item and assert expected task statuses.
        task_route = 0
        task_name = 'task1'
        task_ctx = {'xs': ['fee']}

        task_action_specs = [
            {'action': 'core.echo', 'input': {'message': 'fee'}, 'item_id': 0},
        ]

        # Verify the set of action executions.
        expected_task = self.format_task_item(
            task_name,
            task_route,
            task_ctx,
            conductor.spec.tasks.get_task(task_name),
            actions=task_action_specs,
            items_count=len(task_ctx['xs'])
        )

        expected_tasks = [expected_task]
        actual_tasks = conductor.get_next_tasks()
        self.assert_task_list(conductor, actual_tasks, expected_tasks)

        # Set the item to running status.
        self.forward_task_statuses(conductor, task_name, [statuses.RUNNING], [0])

        # Assert that the task is running.
        actual_task_status = conductor.workflow_state.get_task(task_name, task_route)['status']
        self.assertEqual(actual_task_status, statuses.RUNNING)

        # Change status for the item.
        item_ids = [0]
        results = [task_ctx['xs'][0]]
        status_changes = [ac_ex_status]
        self.forward_task_statuses(conductor, task_name, status_changes, item_ids, results)

        # Assert task and workflow status.
        actual_task_status = conductor.workflow_state.get_task(task_name, task_route)['status']
        self.assertEqual(actual_task_status, tk_ex_status)
        self.assertEqual(conductor.get_workflow_status(), wf_ex_status)
    def test_init_task_with_no_action(self):
        wf_def = """
        version: 1.0

        tasks:
          task1:
            next:
              - publish: xyz=123
                do: task2
          task2:
            action: core.noop
        """

        spec = native_specs.WorkflowSpec(wf_def)
        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # Process task1.
        task_name = "task1"
        expected_task_ctx = {}
        self.assert_next_task(conductor, task_name, expected_task_ctx)
        self.forward_task_statuses(conductor, task_name,
                                   [statuses.RUNNING, statuses.SUCCEEDED])

        # Process task2.
        task_name = "task2"
        expected_task_ctx = {"xyz": 123}
        self.assert_next_task(conductor, task_name, expected_task_ctx)
        self.forward_task_statuses(conductor, task_name,
                                   [statuses.RUNNING, statuses.SUCCEEDED])

        self.assertEqual(conductor.get_workflow_status(), statuses.SUCCEEDED)
Example #3
0
    def test_workflow_not_in_rerunable_status(self):
        wf_def = """
        version: 1.0

        tasks:
          task1:
            action: core.echo message="$RANDOM"
            next:
              - when: <% succeeded() %>
                do: task2
          task2:
            action: core.noop
        """

        fast_forward_success = [statuses.RUNNING, statuses.SUCCEEDED]

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # Succeed task1.
        next_tasks = conductor.get_next_tasks()
        self.forward_task_statuses(conductor, next_tasks[0]['id'], fast_forward_success)

        # Assert rerun cannot happen because workflow is still running.
        self.assertRaises(
            exc.WorkflowIsActiveAndNotRerunableError,
            conductor.request_workflow_rerun
        )
    def _prep_conductor(self, status=None):
        wf_def = """
        version: 1.0

        description: A basic sequential workflow.

        tasks:
          task1:
            action: core.noop
            next:
              - when: <% succeeded() %>
                do: task2
          task2:
            action: core.noop
            next:
              - when: <% succeeded() %>
                do: task3
          task3:
            action: core.noop
        """

        spec = native_specs.WorkflowSpec(wf_def)
        conductor = conducting.WorkflowConductor(spec)

        if status:
            conductor.request_workflow_status(status)

        return conductor
    def test_bad_items_type(self):
        wf_def = """
        version: 1.0

        vars:
          - xs: fee fi fo fum

        tasks:
          task1:
            with: x in <% ctx(xs) %>
            action: core.echo message=<% item(y) %>
        """

        expected_errors = [{
            'type': 'error',
            'message':
            'TypeError: The value of "<% ctx(xs) %>" is not type of list.',
            'task_id': 'task1'
        }]

        spec = specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_state(states.RUNNING)
        tasks = conductor.get_next_tasks()

        self.assertListEqual(tasks, [])
        self.assertEqual(conductor.get_workflow_state(), states.FAILED)
        self.assertListEqual(conductor.errors, expected_errors)
    def test_get_start_tasks_via_get_next_tasks_with_multiple_task_action_and_input_errors(self):
        wf_def = """
        version: 1.0

        description: A basic branching workflow.

        vars:
          - foobar: fubar
          - fubar: foobar

        tasks:
          task1:
            action: <% ctx().foobar.fubar %>
            next:
              - when: <% succeeded() %>
                do: task3
          task2:
            action: core.noop var_x=<% ctx().fubar.foobar %>
            next:
              - when: <% succeeded() %>
                do: task3
          task3:
            join: all
            action: core.noop
        """

        expected_errors = [
            {
                'type': 'error',
                'message': (
                    'YaqlEvaluationException: Unable to evaluate expression '
                    '\'<% ctx().foobar.fubar %>\'. NoFunctionRegisteredException: '
                    'Unknown function "#property#fubar"'
                ),
                'task_id': 'task1'
            },
            {
                'type': 'error',
                'message': (
                    'YaqlEvaluationException: Unable to evaluate expression '
                    '\'<% ctx().fubar.foobar %>\'. NoFunctionRegisteredException: '
                    'Unknown function "#property#foobar"'
                ),
                'task_id': 'task2'
            }
        ]

        spec = specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_state(states.RUNNING)

        # The get_next_tasks method should not return any tasks.
        self.assertListEqual(conductor.get_next_tasks(), [])

        # The workflow should fail with the expected errors.
        self.assertEqual(conductor.get_workflow_state(), states.FAILED)
        actual_errors = sorted(conductor.errors, key=lambda x: x.get('task_id', None))
        self.assertListEqual(actual_errors, expected_errors)
Example #7
0
    def test_runtime_function_of_splits_count(self):
        num_tasks = 25

        wf_def = {"input": ["data"], "tasks": {}}

        for i in range(1, num_tasks):
            task_name = "t" + str(i)
            next_task_name = "t" + str(i + 1)
            next_next_task_name = "t" + str(i + 2)
            transition = [
                {
                    "when": "<% succeeded() %>",
                    "do": next_task_name
                },
                {
                    "when": "<% succeeded() %>",
                    "do": next_next_task_name
                },
            ]
            wf_def["tasks"][task_name] = {
                "action": "core.noop",
                "next": transition
            }

        wf_def["tasks"]["t%d" % num_tasks] = {"action": "core.noop"}
        wf_def["tasks"]["t%d" % (num_tasks + 1)] = {"action": "core.noop"}

        spec = native_specs.WorkflowSpec(wf_def)

        t1 = datetime.datetime.utcnow()
        self.assertDictEqual(spec.inspect(), {})
        t2 = datetime.datetime.utcnow()

        delta = t2 - t1
        self.assertLess(delta.seconds, 3)
    def test_workflow_output_error(self):
        wf_def = """
        version: 1.0

        output:
          - xyz: <% result().foobar %>

        tasks:
          task1:
            action: core.noop
        """

        expected_errors = [{
            'type':
            'error',
            'message':
            ('YaqlEvaluationException: Unable to evaluate expression '
             '\'<% result().foobar %>\'. ExpressionEvaluationException: '
             'The current task is not set in the context.')
        }]

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # Manually complete task1.
        self.forward_task_statuses(conductor, 'task1',
                                   [statuses.RUNNING, statuses.SUCCEEDED])

        self.assertEqual(conductor.get_workflow_status(), statuses.FAILED)
        self.assertListEqual(conductor.errors, expected_errors)
        self.assertIsNone(conductor.get_workflow_output())
    def test_pause_and_failed_with_task_transition_error(self):
        wf_def = """
        version: 1.0
        description: A basic sequential workflow.
        tasks:
          task1:
            action: core.noop
            next:
              - when: <% result().foobar %>
                do: task2
          task2:
            action: core.noop
        """

        spec = native_specs.WorkflowSpec(wf_def)
        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # Run task1.
        self.forward_task_statuses(conductor, 'task1', [statuses.RUNNING])
        self.assertEqual(conductor.get_workflow_status(), statuses.RUNNING)

        # Complete task1 and assert the workflow execution fails
        # due to the expression error in the task transition.
        self.forward_task_statuses(conductor, 'task1', [statuses.SUCCEEDED])
        self.assertEqual(conductor.get_workflow_status(), statuses.FAILED)
Example #10
0
    def test_task_delay_rendering_bad_type(self):
        wf_def = """
        version: 1.0

        description: A basic sequential workflow.

        vars:
          - delay: foobar

        tasks:
          task1:
            delay: <% ctx().delay %>
            action: core.noop
        """

        expected_errors = [{
            'type': 'error',
            'message':
            'TypeError: The value of task delay is not type of integer.',
            'task_id': 'task1'
        }]

        # Instantiate workflow spec.
        spec = specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        # Instantiate conductor
        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_state(states.RUNNING)

        # Assert failed status and errors.
        self.assert_task_list(conductor.get_next_tasks(), [])
        self.assertEqual(conductor.get_workflow_state(), states.FAILED)
        self.assertListEqual(conductor.errors, expected_errors)
    def test_task_delay_rendering_bad_type(self):
        wf_def = """
        version: 1.0

        description: A basic sequential workflow.

        vars:
          - delay: foobar

        tasks:
          task1:
            delay: <% ctx().delay %>
            action: core.noop
        """

        expected_errors = [{
            "type": "error",
            "message":
            "TypeError: The value of task delay is not type of integer.",
            "task_id": "task1",
            "route": 0,
        }]

        # Instantiate workflow spec.
        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        # Instantiate conductor
        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # Assert failed status and errors.
        self.assert_next_task(conductor, has_next_task=False)
        self.assertEqual(conductor.get_workflow_status(), statuses.FAILED)
        self.assertListEqual(conductor.errors, expected_errors)
    def test_workflow_input_seq_ref_error(self):
        wf_def = """
        version: 1.0

        input:
          - x
          - y: <% ctx().x %>
          - z: <% ctx().y.value %>

        tasks:
          task1:
            action: core.noop
        """

        expected_errors = [{
            "type":
            "error",
            "message":
            ("YaqlEvaluationException: Unable to evaluate expression "
             "'<% ctx().y.value %>'. NoFunctionRegisteredException: "
             'Unknown function "#property#value"'),
        }]

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)

        self.assertRaises(exc.InvalidWorkflowStatusTransition,
                          conductor.request_workflow_status, statuses.RUNNING)

        self.assert_next_task(conductor, has_next_task=False)
        self.assertEqual(conductor.get_workflow_status(), statuses.FAILED)
        self.assertListEqual(conductor.errors, expected_errors)
    def test_with_items_that_is_action_less(self):
        wf_def = """
        version: 1.0

        vars:
          - xs:
              - fee
              - fi
              - fo
              - fum

        tasks:
          task1:
            with:
                items: <% ctx(xs) %>
            next:
              - publish:
                  - items: <% result() %>

        output:
          - items: <% ctx(items) %>
        """

        expected_errors = {
            "semantics": [{
                "message":
                "The action property is required for with items task.",
                "schema_path": "properties.tasks.patternProperties.^\\w+$",
                "spec_path": "tasks.task1",
            }]
        }

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), expected_errors)
    def test_runtime_function_of_items_list_size(self):
        wf_def = """
        version: 1.0

        vars:
          - xs: <% range(500).select(str($)) %>

        tasks:
          task1:
            with: <% ctx(xs) %>
            action: core.echo message=<% item() %>
            next:
              - publish:
                  - items: <% result() %>

        output:
          - items: <% ctx(items) %>
        """

        num_items = 500

        spec = specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_state(states.RUNNING)

        # Mock the action execution for each item and assert expected task states.
        task_name = 'task1'
        task_ctx = {'xs': [str(i) for i in range(0, num_items)]}

        task_action_specs = [
            {'action': 'core.echo', 'input': {'message': i}, 'item_id': int(i)}
            for i in task_ctx['xs']
        ]

        mock_ac_ex_states = [states.SUCCEEDED] * num_items
        expected_task_states = [states.RUNNING] * (num_items - 1) + [states.SUCCEEDED]
        expected_workflow_states = [states.RUNNING] * (num_items - 1) + [states.SUCCEEDED]

        self.assert_task_items(
            conductor,
            task_name,
            task_ctx,
            task_ctx['xs'],
            task_action_specs,
            mock_ac_ex_states,
            expected_task_states,
            expected_workflow_states
        )

        # Assert the task is removed from staging.
        self.assertNotIn(task_name, conductor.flow.staged)

        # Assert the workflow succeeded.
        self.assertEqual(conductor.get_workflow_state(), states.SUCCEEDED)

        # Assert the workflow output is correct.
        expected_output = {'items': task_ctx['xs']}
        self.assertDictEqual(conductor.get_workflow_output(), expected_output)
    def test_workflow_input_error(self):
        wf_def = """
        version: 1.0

        input:
          - xyz: <% result().foobar %>

        tasks:
          task1:
            action: core.noop
        """

        expected_errors = [{
            'type':
            'error',
            'message':
            ('YaqlEvaluationException: Unable to evaluate expression '
             '\'<% result().foobar %>\'. ExpressionEvaluationException: '
             'The current task is not set in the context.')
        }]

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)

        self.assertRaises(exc.InvalidWorkflowStatusTransition,
                          conductor.request_workflow_status, statuses.RUNNING)

        self.assertListEqual(conductor.get_next_tasks(), [])
        self.assertEqual(conductor.get_workflow_status(), statuses.FAILED)
        self.assertListEqual(conductor.errors, expected_errors)
    def test_cancel_workflow_already_canceling(self):
        wf_def = """
        version: 1.0

        tasks:
          task1:
            action: core.noop
        """

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        # Run the workflow and keep it running.
        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)
        self.forward_task_statuses(conductor, 'task1', [statuses.RUNNING])

        # Cancels the workflow and complete task1.
        conductor.request_workflow_status(statuses.CANCELING)
        self.assertEqual(conductor.get_workflow_status(), statuses.CANCELING)

        # Cancels the workflow again.
        conductor.request_workflow_status(statuses.CANCELING)
        self.assertEqual(conductor.get_workflow_status(), statuses.CANCELING)
        conductor.request_workflow_status(statuses.CANCELED)
        self.assertEqual(conductor.get_workflow_status(), statuses.CANCELING)

        # Complete task1 and check workflow status.
        self.forward_task_statuses(conductor, 'task1', [statuses.SUCCEEDED])
        self.assertEqual(conductor.get_workflow_status(), statuses.CANCELED)
    def test_workflow_output(self):
        wf_def = """
        version: 1.0

        output:
          - x: 123
          - y: <% ctx().x %>

        tasks:
          task1:
            action: core.noop
        """

        expected_output = {'x': 123, 'y': 123}

        expected_errors = []

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        # Run the workflow and keep it running.
        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)
        self.forward_task_statuses(conductor, 'task1', [statuses.RUNNING])

        # Cancels the workflow and complete task1.
        conductor.request_workflow_status(statuses.CANCELING)
        self.forward_task_statuses(conductor, 'task1', [statuses.SUCCEEDED])

        # Check workflow status and output.
        self.assertEqual(conductor.get_workflow_status(), statuses.CANCELED)
        self.assertListEqual(conductor.errors, expected_errors)
        self.assertDictEqual(conductor.get_workflow_output(), expected_output)
    def test_workflow_vars_error(self):
        wf_def = """
        version: 1.0

        vars:
          - xyz: <% result().foobar %>

        tasks:
          task1:
            action: core.noop
        """

        expected_errors = [{
            "type":
            "error",
            "message":
            ("YaqlEvaluationException: Unable to evaluate expression "
             "'<% result().foobar %>'. ExpressionEvaluationException: "
             "The current task is not set in the context."),
        }]

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)

        self.assertRaises(exc.InvalidWorkflowStatusTransition,
                          conductor.request_workflow_status, statuses.RUNNING)

        self.assert_next_task(conductor, has_next_task=False)
        self.assertEqual(conductor.get_workflow_status(), statuses.FAILED)
        self.assertListEqual(conductor.errors, expected_errors)
    def test_workflow_vars_seq_ref_error(self):
        wf_def = """
        version: 1.0

        vars:
          - x: 123
          - y: <% ctx().x %>
          - z: <% ctx().y.value %>

        tasks:
          task1:
            action: core.noop
        """

        expected_errors = [
            {
                'type': 'error',
                'message': (
                    'YaqlEvaluationException: Unable to evaluate expression '
                    '\'<% ctx().y.value %>\'. NoFunctionRegisteredException: '
                    'Unknown function "#property#value"'
                )
            }
        ]

        spec = specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_state(states.RUNNING)

        self.assertListEqual(conductor.get_next_tasks(), [])
        self.assertEqual(conductor.get_workflow_state(), states.FAILED)
        self.assertListEqual(conductor.errors, expected_errors)
Example #20
0
    def test_get_task_status_of_tasks_along_splits(self):
        wf_def = """
        version: 1.0

        tasks:
          task1:
            action: core.noop
            next:
              - when: <% succeeded() %>
                do: task2
              - when: <% failed() %>
                do: task2
          task2:
            action: core.noop
            next:
              - when: <% succeeded() %>
                do: task3
              - when: <% failed() %>
                do: task3
          task3:
            action: core.noop
            next:
              - publish:
                  - task1_status: <% task_status(task1) %>
                  - task2_status: <% task_status(task2) %>
                  - task3_status: <% task_status(task3) %>

        output:
          - task1_status: <% ctx(task1_status) %>
          - task2_status: <% ctx(task2_status) %>
          - task3_status: <% ctx(task3_status) %>
        """

        expected_errors = []
        expected_output = {
            "task1_status": "succeeded",
            "task2_status": "succeeded",
            "task3_status": "succeeded",
        }

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        # Run the workflow.
        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)
        self.assertEqual(conductor.get_workflow_status(), statuses.RUNNING)
        self.assertListEqual(conductor.errors, expected_errors)

        # Complete tasks
        status_changes = [statuses.RUNNING, statuses.SUCCEEDED]
        self.forward_task_statuses(conductor, "task1", status_changes)
        self.forward_task_statuses(conductor, "task2", status_changes, route=1)
        self.forward_task_statuses(conductor, "task3", status_changes, route=2)

        # Render workflow output and check workflow status and output.
        conductor.render_workflow_output()
        self.assertEqual(conductor.get_workflow_status(), statuses.SUCCEEDED)
        self.assertListEqual(conductor.errors, expected_errors)
        self.assertDictEqual(conductor.get_workflow_output(), expected_output)
    def test_task_transition_publish_error(self):
        wf_def = """
        version: 1.0

        description: A basic branching workflow.

        vars:
          - foobar: fubar

        tasks:
          task1:
            action: core.noop
            next:
              - when: <% succeeded() %>
                publish:
                  - var1: <% ctx().foobar.fubar %>
                do: task2
          task2:
            action: core.noop
            next:
              - when: <% succeeded() %>
                publish:
                  - var2: 123
        """

        expected_errors = [{
            "type":
            "error",
            "message":
            ("YaqlEvaluationException: Unable to evaluate expression "
             "'<% ctx().foobar.fubar %>'. NoFunctionRegisteredException: "
             'Unknown function "#property#fubar"'),
            "route":
            0,
            "task_id":
            "task1",
            "task_transition_id":
            "task2__t0",
        }]

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # The workflow should fail on completion of task1 while evaluating task transition.
        self.forward_task_statuses(conductor, "task1",
                                   [statuses.RUNNING, statuses.SUCCEEDED])

        # The workflow should fail with the expected errors.
        self.assertEqual(conductor.get_workflow_status(), statuses.FAILED)
        actual_errors = sorted(conductor.errors,
                               key=lambda x: x.get("task_id", None))
        self.assertListEqual(actual_errors, expected_errors)
        self.assertNotIn("task2", conductor.workflow_state.staged)

        # Since the workflow failed, there should be no next tasks returned.
        self.assert_next_task(conductor, has_next_task=False)
    def test_empty_items_list(self):
        wf_def = """
        version: 1.0

        vars:
          - xs: []

        tasks:
          task1:
            with: <% ctx(xs) %>
            action: core.echo message=<% item() %>
            next:
              - publish:
                  - items: <% result() %>

        output:
          - items: <% ctx(items) %>
        """

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # Mock the action execution for each item and assert expected task statuses.
        task_route = 0
        task_name = "task1"
        task_ctx = {"xs": []}
        task_action_specs = []

        mock_ac_ex_statuses = []
        expected_task_statuses = [statuses.SUCCEEDED]
        expected_workflow_statuses = [statuses.SUCCEEDED]

        self.assert_task_items(
            conductor,
            task_name,
            task_route,
            task_ctx,
            task_ctx["xs"],
            task_action_specs,
            mock_ac_ex_statuses,
            expected_task_statuses,
            expected_workflow_statuses,
        )

        # Assert the task is removed from staging.
        self.assertIsNone(
            conductor.workflow_state.get_staged_task(task_name, task_route))

        # Assert the workflow succeeded.
        self.assertEqual(conductor.get_workflow_status(), statuses.SUCCEEDED)

        # Assert the workflow output is correct.
        conductor.render_workflow_output()
        expected_output = {"items": []}
        self.assertDictEqual(conductor.get_workflow_output(), expected_output)
Example #23
0
    def test_task_transition_publish_seq_ref_error(self):
        wf_def = """
        version: 1.0

        description: A basic branching workflow.

        vars:
          - foobar: fubar

        tasks:
          task1:
            action: core.noop
            next:
              - when: <% succeeded() %>
                publish:
                  - x: 123
                  - y: <% ctx().x %>
                  - z: <% ctx().y.value %>
                do: task2
          task2:
            action: core.noop
            next:
              - when: <% succeeded() %>
                publish:
                  - var2: 123
        """

        expected_errors = [
            {
                'type': 'error',
                'message': (
                    'YaqlEvaluationException: Unable to evaluate expression '
                    '\'<% ctx().y.value %>\'. NoFunctionRegisteredException: '
                    'Unknown function "#property#value"'
                ),
                'route': 0,
                'task_id': 'task1',
                'task_transition_id': 'task2__t0'
            }
        ]

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # The workflow should fail on completion of task1 while evaluating task transition.
        self.forward_task_statuses(conductor, 'task1', [statuses.RUNNING, statuses.SUCCEEDED])

        # The workflow should fail with the expected errors.
        self.assertEqual(conductor.get_workflow_status(), statuses.FAILED)
        actual_errors = sorted(conductor.errors, key=lambda x: x.get('task_id', None))
        self.assertListEqual(actual_errors, expected_errors)
        self.assertNotIn('task2', conductor.workflow_state.staged)

        # Since the workflow failed, there should be no next tasks returned.
        self.assert_next_task(conductor, has_next_task=False)
    def test_fail_one_and_only_item(self):
        wf_def = """
        version: 1.0

        vars:
          - xs:
              - fee

        tasks:
          task1:
            with: <% ctx(xs) %>
            action: core.echo message=<% item() %>
        """

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # Mock the action execution for each item and assert expected task statuses.
        task_route = 0
        task_name = "task1"
        task_ctx = {"xs": ["fee"]}

        task_action_specs = [
            {
                "action": "core.echo",
                "input": {
                    "message": "fee"
                },
                "item_id": 0
            },
        ]

        mock_ac_ex_statuses = [statuses.FAILED]
        expected_task_statuses = [statuses.FAILED]
        expected_workflow_statuses = [statuses.FAILED]

        self.assert_task_items(
            conductor,
            task_name,
            task_route,
            task_ctx,
            task_ctx["xs"],
            task_action_specs,
            mock_ac_ex_statuses,
            expected_task_statuses,
            expected_workflow_statuses,
        )

        # Assert the task is not removed from staging. This is intentional so the with items
        # task can be rerun partially for failed items or items that hasn't been run.
        self.assertIsNotNone(
            conductor.workflow_state.get_staged_task(task_name, task_route))

        # Assert the workflow failed.
        self.assertEqual(conductor.get_workflow_status(), statuses.FAILED)
Example #25
0
    def test_pause_workflow_already_pausing(self):
        wf_def = """
        version: 1.0
        description: A basic branching workflow.
        tasks:
          # branch 1
          task1:
            action: core.noop
            next:
              - when: <% succeeded() %>
                do: task3
          # branch 2
          task2:
            action: core.noop
            next:
              - when: <% succeeded() %>
                do: task3
          # adjoining branch
          task3:
            join: all
            action: core.noop
        """

        spec = native_specs.WorkflowSpec(wf_def)
        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # Run task1 and task2.
        self.forward_task_statuses(conductor, "task1", [statuses.RUNNING])
        self.forward_task_statuses(conductor, "task2", [statuses.RUNNING])
        self.assertEqual(conductor.get_workflow_status(), statuses.RUNNING)

        # Pause the workflow.
        conductor.request_workflow_status(statuses.PAUSING)

        # Complete task1 only. The workflow should still be pausing
        # because task2 is still running.
        self.forward_task_statuses(conductor, "task1", [statuses.SUCCEEDED])
        self.assertEqual(conductor.get_workflow_status(), statuses.PAUSING)

        # Pause the workflow.
        conductor.request_workflow_status(statuses.PAUSING)
        self.assertEqual(conductor.get_workflow_status(), statuses.PAUSING)
        conductor.request_workflow_status(statuses.PAUSED)
        self.assertEqual(conductor.get_workflow_status(), statuses.PAUSING)

        # Complete task2. When task2 completes, the workflow should be paused
        # because there is no task in active status.
        self.forward_task_statuses(conductor, "task2", [statuses.SUCCEEDED])
        self.assertEqual(conductor.get_workflow_status(), statuses.PAUSED)

        # Resume the workflow, task3 should be staged, and complete task3.
        conductor.request_workflow_status(statuses.RESUMING)
        self.assert_next_task(conductor, "task3", {})
        self.forward_task_statuses(conductor, "task3",
                                   [statuses.RUNNING, statuses.SUCCEEDED])
        self.assertEqual(conductor.get_workflow_status(), statuses.SUCCEEDED)
Example #26
0
    def test_with_items_rendering(self):
        wf_def = """
        version: 1.0

        input:
          - xs
          - concurrency

        tasks:
          task1:
            with:
              items: <% ctx(xs) %>
              concurrency: <% ctx(concurrency) %>
            action: core.echo message=<% item() %>
            next:
              - publish:
                  - items: <% result() %>

        output:
          - items: <% ctx(items) %>
        """

        # Instantiate workflow spec.
        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        # Instantiate conductor
        inputs = {'xs': ['fee', 'fi', 'fo', 'fum'], 'concurrency': 2}
        conductor = conducting.WorkflowConductor(spec, inputs=inputs)
        conductor.request_workflow_status(statuses.RUNNING)

        # Test that the items and context of a task is rendered from context.
        task_route = 0
        next_task_name = 'task1'
        next_task_ctx = inputs
        next_task_spec = conductor.spec.tasks.get_task(next_task_name)

        next_task_action_specs = [
            {'action': 'core.echo', 'input': {'message': 'fee'}, 'item_id': 0},
            {'action': 'core.echo', 'input': {'message': 'fi'}, 'item_id': 1},
            {'action': 'core.echo', 'input': {'message': 'fo'}, 'item_id': 2},
            {'action': 'core.echo', 'input': {'message': 'fum'}, 'item_id': 3},
        ]

        expected_task = self.format_task_item(
            next_task_name,
            task_route,
            next_task_ctx,
            next_task_spec,
            actions=next_task_action_specs[0:inputs['concurrency']],
            items_count=len(inputs['xs']),
            items_concurrency=inputs['concurrency']
        )

        expected_tasks = [expected_task]

        self.assert_task_list(conductor, conductor.get_next_tasks(), expected_tasks)
    def test_failed_item_task_dormant_other_failed(self):
        wf_def = """
        version: 1.0

        vars:
          - xs:
              - fee
              - fi
              - fo
              - fum

        tasks:
          task1:
            with: <% ctx(xs) %>
            action: core.echo message=<% item() %>
        """

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # Mock the action execution for each item and assert expected task statuses.
        task_route = 0
        task_name = 'task1'
        task_ctx = {'xs': ['fee', 'fi', 'fo', 'fum']}

        task_action_specs = [
            {'action': 'core.echo', 'input': {'message': 'fee'}, 'item_id': 0},
            {'action': 'core.echo', 'input': {'message': 'fi'}, 'item_id': 1},
            {'action': 'core.echo', 'input': {'message': 'fo'}, 'item_id': 2},
            {'action': 'core.echo', 'input': {'message': 'fum'}, 'item_id': 3},
        ]

        mock_ac_ex_statuses = [statuses.SUCCEEDED, statuses.FAILED, statuses.FAILED]
        expected_task_statuses = [statuses.RUNNING, statuses.RUNNING, statuses.FAILED]
        expected_workflow_statuses = [statuses.RUNNING, statuses.RUNNING, statuses.FAILED]

        self.assert_task_items(
            conductor,
            task_name,
            task_route,
            task_ctx,
            task_ctx['xs'],
            task_action_specs,
            mock_ac_ex_statuses,
            expected_task_statuses,
            expected_workflow_statuses
        )

        # Assert the task is not removed from staging. This is intentional so the with items
        # task can be rerun partially for failed items or items that hasn't been run.
        self.assertIsNotNone(conductor.workflow_state.get_staged_task(task_name, task_route))

        # Assert the workflow failed.
        self.assertEqual(conductor.get_workflow_status(), statuses.FAILED)
    def test_task_transition_publish_seq_ref_error(self):
        wf_def = """
        version: 1.0

        description: A basic branching workflow.

        vars:
          - foobar: fubar

        tasks:
          task1:
            action: core.noop
            next:
              - when: <% succeeded() %>
                publish:
                  - x: 123
                  - y: <% ctx().x %>
                  - z: <% ctx().y.value %>
                do: task2
          task2:
            action: core.noop
            next:
              - when: <% succeeded() %>
                publish:
                  - var2: 123
        """

        expected_errors = [
            {
                'type': 'error',
                'message': (
                    'YaqlEvaluationException: Unable to evaluate expression '
                    '\'<% ctx().y.value %>\'. NoFunctionRegisteredException: '
                    'Unknown function "#property#value"'
                ),
                'task_id': 'task1',
                'task_transition_id': 'task2__0'
            }
        ]

        spec = specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_state(states.RUNNING)

        # The workflow should fail on completion of task1 while evaluating task transition.
        task_name = 'task1'
        conductor.update_task_flow(task_name, events.ActionExecutionEvent(states.RUNNING))
        conductor.update_task_flow(task_name, events.ActionExecutionEvent(states.SUCCEEDED))

        # The workflow should fail with the expected errors.
        self.assertEqual(conductor.get_workflow_state(), states.FAILED)
        actual_errors = sorted(conductor.errors, key=lambda x: x.get('task_id', None))
        self.assertListEqual(actual_errors, expected_errors)
        self.assertNotIn('task2', conductor.flow.staged)
        self.assertListEqual(conductor.get_next_tasks(), [])
    def test_pause_and_resume_from_branches(self):
        wf_def = """
        version: 1.0
        description: A basic branching workflow.
        tasks:
          # branch 1
          task1:
            action: core.noop
            next:
              - when: <% succeeded() %>
                do: task3
          # branch 2
          task2:
            action: core.noop
            next:
              - when: <% succeeded() %>
                do: task3
          # adjoining branch
          task3:
            join: all
            action: core.noop
        """

        spec = native_specs.WorkflowSpec(wf_def)
        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)

        # Run task1 and task2.
        self.forward_task_statuses(conductor, 'task1', [statuses.RUNNING])
        self.forward_task_statuses(conductor, 'task2', [statuses.RUNNING])
        self.assertEqual(conductor.get_workflow_status(), statuses.RUNNING)

        # Pause task1 and task2.
        self.forward_task_statuses(conductor, 'task1', [statuses.PAUSED])
        self.forward_task_statuses(conductor, 'task2', [statuses.PAUSED])
        self.assertEqual(conductor.get_workflow_status(), statuses.PAUSED)

        # Resume and complete task1 only. Once task1 completes, the workflow
        # should pause again because there is no active task.
        self.forward_task_statuses(conductor, 'task1', [statuses.RUNNING])
        self.assertEqual(conductor.get_workflow_status(), statuses.RUNNING)
        self.forward_task_statuses(conductor, 'task1', [statuses.SUCCEEDED])
        self.assertEqual(conductor.get_workflow_status(), statuses.PAUSED)

        # Resume and complete task2. When task2 completes, the workflow
        # should stay running because task3 is now staged and ready.
        self.forward_task_statuses(conductor, 'task2', [statuses.RUNNING])
        self.assertEqual(conductor.get_workflow_status(), statuses.RUNNING)
        self.forward_task_statuses(conductor, 'task2', [statuses.SUCCEEDED])
        self.assertEqual(conductor.get_workflow_status(), statuses.RUNNING)
        self.assert_next_task(conductor, 'task3', {})

        # Complete task3.
        self.forward_task_statuses(conductor, 'task3',
                                   [statuses.RUNNING, statuses.SUCCEEDED])
        self.assertEqual(conductor.get_workflow_status(), statuses.SUCCEEDED)
Example #30
0
    def test_get_task_status_at_various_locations(self):
        wf_def = """
        version: 1.0

        tasks:
          task1:
            action: core.noop
            next:
              - when: <% succeeded() %>
                publish: task_status_at_publish=<% task_status(task1) %>
                do: task2
          task2:
            action: core.echo
            input:
              message: <% task_status(task1) %>

        output:
          - task_status_at_publish: <% ctx().task_status_at_publish %>
          - task_status_at_output: <% task_status(task1) %>
        """

        expected_errors = []
        expected_output = {
            'task_status_at_publish': 'succeeded',
            'task_status_at_output': 'succeeded'
        }

        spec = native_specs.WorkflowSpec(wf_def)
        self.assertDictEqual(spec.inspect(), {})

        # Run the workflow.
        conductor = conducting.WorkflowConductor(spec)
        conductor.request_workflow_status(statuses.RUNNING)
        self.assertEqual(conductor.get_workflow_status(), statuses.RUNNING)
        self.assertListEqual(conductor.errors, expected_errors)

        # Complete task1.
        self.forward_task_statuses(conductor, 'task1',
                                   [statuses.RUNNING, statuses.SUCCEEDED])

        # Get next tasks and ensure task_status return expected result in task rendering.
        task2_ex_req = conductor.get_next_tasks()[0]
        self.assertEqual(task2_ex_req['id'], 'task2')
        self.assertEqual(task2_ex_req['actions'][0]['action'], 'core.echo')
        self.assertEqual(task2_ex_req['actions'][0]['input']['message'],
                         statuses.SUCCEEDED)

        # Complete task2.
        self.forward_task_statuses(conductor, 'task2',
                                   [statuses.RUNNING, statuses.SUCCEEDED])

        # Check workflow status and output.
        self.assertEqual(conductor.get_workflow_status(), statuses.SUCCEEDED)
        self.assertListEqual(conductor.errors, expected_errors)
        self.assertDictEqual(conductor.get_workflow_output(), expected_output)