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_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 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)
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_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 assert_workflow_state(self, wf_name, mock_flow, expected_wf_states, conductor=None): if not conductor: wf_def = self.get_wf_def(wf_name) wf_spec = self.spec_module.instantiate(wf_def) conductor = conducting.WorkflowConductor(wf_spec) conductor.request_workflow_state(states.RUNNING) for task_flow_entry, expected_wf_state in zip(mock_flow, expected_wf_states): task_id = task_flow_entry['id'] task_state = task_flow_entry['state'] ac_ex_event = events.ActionExecutionEvent(task_state) conductor.update_task_flow(task_id, ac_ex_event) err_ctx = ('Workflow state "%s" is not the expected state "%s". ' 'Updated task "%s" with state "%s".' % (conductor.get_workflow_state(), expected_wf_state, task_id, task_state)) self.assertEqual(conductor.get_workflow_state(), expected_wf_state, err_ctx) return conductor
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 _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_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 assert_workflow_status(self, wf_name, mock_flow, expected_wf_statuses, conductor=None): if not conductor: wf_def = self.get_wf_def(wf_name) wf_spec = self.spec_module.instantiate(wf_def) conductor = conducting.WorkflowConductor(wf_spec) conductor.request_workflow_status(statuses.RUNNING) for _entry, expected_wf_status in zip(mock_flow, expected_wf_statuses): task_id = _entry['id'] task_status = _entry['status'] self.forward_task_statuses(conductor, task_id, [task_status]) err_ctx = ('Workflow status "%s" is not the expected status "%s". ' 'Updated task "%s" with status "%s".' % (conductor.get_workflow_status(), expected_wf_status, task_id, task_status)) self.assertEqual(conductor.get_workflow_status(), expected_wf_status, err_ctx) return conductor
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_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)
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)
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_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_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)
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_run_command_task_is_failed(self): # initialize workflow conductor conductor = conducting.WorkflowConductor(**{ 'spec': self.spec, 'inputs': { 'cmd': 'test_command', 'host': 'test.example.com', 'slack_channel': '#hoge' }, }) # initialize workflow status conductor.request_workflow_status(statuses.RUNNING) # get task informations to be run at first next_tasks = conductor.get_next_tasks() # finish run_command with failure and get next task self.forward_task_statuses(conductor, 'run_command', [statuses.RUNNING, statuses.FAILED]) next_tasks = conductor.get_next_tasks() self.assertEqual(len(next_tasks), 1) self.assertEqual(next_tasks[0]['id'], 'report_error') self.assertEqual(len(next_tasks[0]['actions']), 1) _action_info = next_tasks[0]['actions'][0] self.assertEqual(_action_info['action'], 'slack.post_message') self.assertEqual(_action_info['input']['channel'], '#hoge') self.assertEqual(_action_info['input']['message'], ( '===[ ERROR ]===\n' 'run_command task was failed to run command ' '("test_command") on test.example.com\n' ))
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_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_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)
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)
def test_run_command_task_is_succeeded(self): # initialize workflow conductor conductor = conducting.WorkflowConductor(**{ 'spec': self.spec, 'inputs': { 'cmd': 'test_command', 'host': 'test.example.com', 'slack_channel': '#hoge' }, }) # initialize workflow status conductor.request_workflow_status(statuses.RUNNING) # get task informations to be run at first next_tasks = conductor.get_next_tasks() # This confirms whether expected action is specified self.assertEqual(len(next_tasks), 1) self.assertEqual(next_tasks[0]['id'], 'run_command') # When 'with' parameter is specified in action, 'actions' might have params more than 1. self.assertEqual(len(next_tasks[0]['actions']), 1) _action_info = next_tasks[0]['actions'][0] self.assertEqual(_action_info['action'], 'core.remote') self.assertEqual(_action_info['input']['cmd'], 'test_command') self.assertEqual(_action_info['input']['hosts'], 'test.example.com') # finish run_command with successful self.forward_task_statuses(conductor, 'run_command', [statuses.RUNNING]) self.forward_task_statuses(conductor, 'run_command', [statuses.SUCCEEDED], **{ 'result': { 'test.example.com': { 'stdout': 'test_output' } } }) # get next running task after run_command is finished successfully next_tasks = conductor.get_next_tasks() # This confirms whether expected action is specified self.assertEqual(len(next_tasks), 1) self.assertEqual(next_tasks[0]['id'], 'report_result') self.assertEqual(len(next_tasks[0]['actions']), 1) _action_info = next_tasks[0]['actions'][0] self.assertEqual(_action_info['action'], 'slack.post_message') self.assertEqual(_action_info['input']['channel'], '#hoge') self.assertEqual(_action_info['input']['message'], ( '===[ SUCCEEDED ]===\n' '* (command) "test_command" on test.example.com\n' '* (output) test_output\n' )) # This confirms after finishing report_result there is no task to be run self.forward_task_statuses(conductor, 'report_result', [statuses.RUNNING, statuses.SUCCEEDED]) self.assertEqual(conductor.get_next_tasks(), [])
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_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_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_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)
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)