340 lines
10 KiB
Python
340 lines
10 KiB
Python
# Copyright 2015 - Mirantis, Inc.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
# you may not use this file except in compliance with the License.
|
|
# You may obtain a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
# See the License for the specific language governing permissions and
|
|
# limitations under the License.
|
|
|
|
import mock
|
|
from oslo_config import cfg
|
|
|
|
from mistral.actions import std_actions
|
|
from mistral.db.v2 import api as db_api
|
|
from mistral.db.v2.sqlalchemy import models
|
|
from mistral import exceptions as exc
|
|
from mistral.services import actions
|
|
from mistral.tests.unit.engine import base
|
|
from mistral.workflow import states
|
|
from mistral_lib import actions as ml_actions
|
|
|
|
# Use the set_default method to set value otherwise in certain test cases
|
|
# the change in value is not permanent.
|
|
cfg.CONF.set_default('auth_enable', False, group='pecan')
|
|
|
|
|
|
class RunActionEngineTest(base.EngineTestCase):
|
|
@classmethod
|
|
def heavy_init(cls):
|
|
super(RunActionEngineTest, cls).heavy_init()
|
|
|
|
action = """---
|
|
version: '2.0'
|
|
|
|
concat:
|
|
base: std.echo
|
|
base-input:
|
|
output: <% $.left %><% $.right %>
|
|
input:
|
|
- left
|
|
- right
|
|
|
|
concat3:
|
|
base: concat
|
|
base-input:
|
|
left: <% $.left %><% $.center %>
|
|
right: <% $.right %>
|
|
input:
|
|
- left
|
|
- center
|
|
- right
|
|
|
|
concat4:
|
|
base: concat3
|
|
base-input:
|
|
left: <% $.left %>
|
|
center: <% $.center_left %><% $.center_right %>
|
|
right: <% $.right %>
|
|
input:
|
|
- left
|
|
- center_left
|
|
- center_right
|
|
- right
|
|
|
|
missing_base:
|
|
base: wrong
|
|
input:
|
|
- some_input
|
|
|
|
nested_missing_base:
|
|
base: missing_base
|
|
input:
|
|
- some_input
|
|
|
|
loop_action:
|
|
base: loop_action
|
|
base-input:
|
|
output: <% $.output %>
|
|
input:
|
|
- output
|
|
|
|
level2_loop_action:
|
|
base: loop_action
|
|
base-input:
|
|
output: <% $.output %>
|
|
input:
|
|
- output
|
|
"""
|
|
actions.create_actions(action)
|
|
|
|
def test_run_action_sync(self):
|
|
# Start action and see the result.
|
|
action_ex = self.engine.start_action('std.echo', {'output': 'Hello!'})
|
|
|
|
self.assertEqual('Hello!', action_ex.output['result'])
|
|
self.assertEqual(states.SUCCESS, action_ex.state)
|
|
|
|
@mock.patch.object(
|
|
std_actions.EchoAction,
|
|
'run',
|
|
mock.Mock(side_effect=exc.ActionException("some error"))
|
|
)
|
|
def test_run_action_error(self):
|
|
# Start action and see the result.
|
|
action_ex = self.engine.start_action('std.echo', {'output': 'Hello!'})
|
|
|
|
self.assertIsNotNone(action_ex.output)
|
|
self.assertIn('some error', action_ex.output['result'])
|
|
self.assertEqual(states.ERROR, action_ex.state)
|
|
|
|
def test_run_action_save_result(self):
|
|
# Start action.
|
|
action_ex = self.engine.start_action(
|
|
'std.echo',
|
|
{'output': 'Hello!'},
|
|
save_result=True
|
|
)
|
|
|
|
self.await_action_success(action_ex.id)
|
|
|
|
with db_api.transaction():
|
|
action_ex = db_api.get_action_execution(action_ex.id)
|
|
|
|
self.assertEqual(states.SUCCESS, action_ex.state)
|
|
self.assertEqual({'result': 'Hello!'}, action_ex.output)
|
|
|
|
def test_run_action_run_sync(self):
|
|
# Start action.
|
|
action_ex = self.engine.start_action(
|
|
'std.echo',
|
|
{'output': 'Hello!'},
|
|
run_sync=True
|
|
)
|
|
|
|
self.assertEqual('Hello!', action_ex.output['result'])
|
|
self.assertEqual(states.SUCCESS, action_ex.state)
|
|
|
|
def test_run_action_save_result_and_run_sync(self):
|
|
# Start action.
|
|
action_ex = self.engine.start_action(
|
|
'std.echo',
|
|
{'output': 'Hello!'},
|
|
save_result=True,
|
|
run_sync=True
|
|
)
|
|
|
|
self.assertEqual('Hello!', action_ex.output['result'])
|
|
self.assertEqual(states.SUCCESS, action_ex.state)
|
|
|
|
with db_api.transaction():
|
|
action_ex = db_api.get_action_execution(action_ex.id)
|
|
|
|
self.assertEqual(states.SUCCESS, action_ex.state)
|
|
self.assertEqual({'result': 'Hello!'}, action_ex.output)
|
|
|
|
def test_run_action_run_sync_error(self):
|
|
# Start action.
|
|
self.assertRaises(
|
|
exc.InputException,
|
|
self.engine.start_action,
|
|
'std.async_noop',
|
|
{},
|
|
run_sync=True
|
|
)
|
|
|
|
def test_run_action_async(self):
|
|
action_ex = self.engine.start_action('std.async_noop', {})
|
|
|
|
self.await_action_state(action_ex.id, states.RUNNING)
|
|
|
|
action_ex = db_api.get_action_execution(action_ex.id)
|
|
|
|
self.assertEqual(states.RUNNING, action_ex.state)
|
|
|
|
@mock.patch.object(
|
|
std_actions.AsyncNoOpAction, 'run',
|
|
mock.MagicMock(side_effect=exc.ActionException('Invoke failed.')))
|
|
def test_run_action_async_invoke_failure(self):
|
|
action_ex = self.engine.start_action('std.async_noop', {})
|
|
|
|
self.await_action_error(action_ex.id)
|
|
|
|
with db_api.transaction():
|
|
action_ex = db_api.get_action_execution(action_ex.id)
|
|
|
|
self.assertEqual(states.ERROR, action_ex.state)
|
|
self.assertIn('Invoke failed.', action_ex.output.get('result', ''))
|
|
|
|
@mock.patch.object(
|
|
std_actions.AsyncNoOpAction, 'run',
|
|
mock.MagicMock(return_value=ml_actions.Result(error='Invoke erred.')))
|
|
def test_run_action_async_invoke_with_error(self):
|
|
action_ex = self.engine.start_action('std.async_noop', {})
|
|
|
|
self.await_action_error(action_ex.id)
|
|
|
|
with db_api.transaction():
|
|
action_ex = db_api.get_action_execution(action_ex.id)
|
|
|
|
self.assertEqual(states.ERROR, action_ex.state)
|
|
self.assertIn('Invoke erred.', action_ex.output.get('result', ''))
|
|
|
|
def test_run_action_adhoc(self):
|
|
# Start action and see the result.
|
|
action_ex = self.engine.start_action(
|
|
'concat',
|
|
{'left': 'Hello, ', 'right': 'John Doe!'}
|
|
)
|
|
|
|
self.assertEqual('Hello, John Doe!', action_ex.output['result'])
|
|
|
|
def test_run_level_two_action_adhoc(self):
|
|
# Start action and see the result.
|
|
action_ex = self.engine.start_action(
|
|
'concat3',
|
|
{'left': 'Hello, ', 'center': 'John', 'right': ' Doe!'}
|
|
)
|
|
|
|
self.assertEqual('Hello, John Doe!', action_ex.output['result'])
|
|
|
|
def test_run_level_three_action_adhoc(self):
|
|
# Start action and see the result.
|
|
action_ex = self.engine.start_action(
|
|
'concat4',
|
|
{
|
|
'left': 'Hello, ',
|
|
'center_left': 'John',
|
|
'center_right': ' Doe',
|
|
'right': '!'
|
|
}
|
|
)
|
|
|
|
self.assertEqual('Hello, John Doe!', action_ex.output['result'])
|
|
|
|
def test_run_action_with_missing_base(self):
|
|
# Start action and see the result.
|
|
self.assertRaises(
|
|
exc.InvalidActionException,
|
|
self.engine.start_action,
|
|
'missing_base',
|
|
{'some_input': 'Hi'}
|
|
)
|
|
|
|
def test_run_action_with_missing_nested_base(self):
|
|
# Start action and see the result.
|
|
self.assertRaises(
|
|
exc.InvalidActionException,
|
|
self.engine.start_action,
|
|
'nested_missing_base',
|
|
{'some_input': 'Hi'}
|
|
)
|
|
|
|
def test_run_loop_action(self):
|
|
# Start action and see the result.
|
|
self.assertRaises(
|
|
ValueError,
|
|
self.engine.start_action,
|
|
'loop_action',
|
|
{'output': 'Hello'}
|
|
)
|
|
|
|
def test_run_level_two_loop_action(self):
|
|
# Start action and see the result.
|
|
self.assertRaises(
|
|
ValueError,
|
|
self.engine.start_action,
|
|
'level2_loop_action',
|
|
{'output': 'Hello'}
|
|
)
|
|
|
|
def test_run_action_wrong_input(self):
|
|
# Start action and see the result.
|
|
exception = self.assertRaises(
|
|
exc.InputException,
|
|
self.engine.start_action,
|
|
'std.http',
|
|
{'url': 'Hello, ', 'metod': 'John Doe!'}
|
|
)
|
|
|
|
self.assertIn('std.http', str(exception))
|
|
|
|
def test_adhoc_action_wrong_input(self):
|
|
# Start action and see the result.
|
|
exception = self.assertRaises(
|
|
exc.InputException,
|
|
self.engine.start_action,
|
|
'concat',
|
|
{'left': 'Hello, ', 'ri': 'John Doe!'}
|
|
)
|
|
|
|
self.assertIn('concat', str(exception))
|
|
|
|
# TODO(rakhmerov): This is an example of a bad test. It pins to
|
|
# implementation details too much and prevents from making refactoring
|
|
# easily. When writing tests we should make assertions about
|
|
# consequences, not about how internal machinery works, i.e. we need to
|
|
# follow "black box" testing paradigm.
|
|
@mock.patch('mistral.engine.actions.resolve_action_definition')
|
|
@mock.patch('mistral.engine.utils.validate_input')
|
|
@mock.patch('mistral.services.action_manager.get_action_class')
|
|
@mock.patch('mistral.engine.actions.PythonAction.run')
|
|
def test_run_action_with_kwargs_input(self, run_mock, class_mock,
|
|
validate_mock, def_mock):
|
|
action_def = models.ActionDefinition()
|
|
action_def.update({
|
|
'name': 'fake_action',
|
|
'action_class': '',
|
|
'attributes': {},
|
|
'description': '',
|
|
'input': '**kwargs',
|
|
'is_system': True,
|
|
'scope': 'public'
|
|
})
|
|
def_mock.return_value = action_def
|
|
run_mock.return_value = ml_actions.Result(data='Hello')
|
|
|
|
class_ret = mock.MagicMock()
|
|
class_mock.return_value = class_ret
|
|
|
|
self.engine.start_action('fake_action', {'input': 'Hello'})
|
|
|
|
self.assertEqual(1, def_mock.call_count)
|
|
def_mock.assert_called_with('fake_action')
|
|
|
|
self.assertEqual(0, validate_mock.call_count)
|
|
|
|
class_ret.assert_called_once_with(input='Hello')
|
|
|
|
run_mock.assert_called_once_with(
|
|
{'input': 'Hello'},
|
|
None,
|
|
save=False,
|
|
timeout=None
|
|
)
|