Passed
Push — develop ( b8d4ca...421369 )
by Plexxi
07:01 queued 03:57
created

Action2.run()   A

Complexity

Conditions 1

Size

Total Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 1
c 3
b 1
f 0
dl 0
loc 2
rs 10
1
# Licensed to the StackStorm, Inc ('StackStorm') under one or more
2
# contributor license agreements.  See the NOTICE file distributed with
3
# this work for additional information regarding copyright ownership.
4
# The ASF licenses this file to You under the Apache License, Version 2.0
5
# (the "License"); you may not use this file except in compliance with
6
# the License.  You may obtain a copy of the License at
7
#
8
#     http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
import os
17
18
import mock
19
20
import python_runner
21
from st2common.runners.python_action_wrapper import PythonActionWrapper
22
from st2common.runners.base_action import Action
23
from st2actions.container import service
24
from st2common.runners.utils import get_action_class_instance
25
from st2common.services import config as config_service
26
from st2common.constants.action import ACTION_OUTPUT_RESULT_DELIMITER
27
from st2common.constants.action import LIVEACTION_STATUS_SUCCEEDED, LIVEACTION_STATUS_FAILED
28
from st2common.constants.action import LIVEACTION_STATUS_TIMED_OUT
29
from st2common.constants.pack import SYSTEM_PACK_NAME
30
from st2tests.base import RunnerTestCase
31
from st2tests.base import CleanDbTestCase
32
import st2tests.base as tests_base
33
34
35
PASCAL_ROW_ACTION_PATH = os.path.join(tests_base.get_resources_path(), 'packs',
36
                                      'pythonactions/actions/pascal_row.py')
37
TEST_ACTION_PATH = os.path.join(tests_base.get_resources_path(), 'packs',
38
                                'pythonactions/actions/test.py')
39
40
# Note: runner inherits parent args which doesn't work with tests since test pass additional
41
# unrecognized args
42
mock_sys = mock.Mock()
43
mock_sys.argv = []
44
45
46
@mock.patch('python_runner.sys', mock_sys)
47
class PythonRunnerTestCase(RunnerTestCase, CleanDbTestCase):
48
    register_packs = True
49
    register_pack_configs = True
50
51
    def test_runner_creation(self):
52
        runner = python_runner.get_runner()
53
        self.assertTrue(runner is not None, 'Creation failed. No instance.')
54
        self.assertEqual(type(runner), python_runner.PythonRunner, 'Creation failed. No instance.')
55
56
    def test_simple_action_with_result_no_status(self):
57
        runner = python_runner.get_runner()
58
        runner.action = self._get_mock_action_obj()
59
        runner.runner_parameters = {}
60
        runner.entry_point = PASCAL_ROW_ACTION_PATH
61
        runner.container_service = service.RunnerContainerService()
62
        runner.pre_run()
63
        (status, output, _) = runner.run({'row_index': 5})
64
        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
65
        self.assertTrue(output is not None)
66
        self.assertEqual(output['result'], [1, 5, 10, 10, 5, 1])
67
68
    def test_simple_action_with_result_as_None_no_status(self):
69
        runner = python_runner.get_runner()
70
        runner.action = self._get_mock_action_obj()
71
        runner.runner_parameters = {}
72
        runner.entry_point = PASCAL_ROW_ACTION_PATH
73
        runner.container_service = service.RunnerContainerService()
74
        runner.pre_run()
75
        (status, output, _) = runner.run({'row_index': 'b'})
76
        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
77
        self.assertTrue(output is not None)
78
        self.assertEqual(output['exit_code'], 0)
79
        self.assertEqual(output['result'], None)
80
81
    def test_simple_action_timeout(self):
82
        timeout = 0
83
        runner = python_runner.get_runner()
84
        runner.action = self._get_mock_action_obj()
85
        runner.runner_parameters = {python_runner.RUNNER_TIMEOUT: timeout}
86
        runner.entry_point = PASCAL_ROW_ACTION_PATH
87
        runner.container_service = service.RunnerContainerService()
88
        runner.pre_run()
89
        (status, output, _) = runner.run({'row_index': 4})
90
        self.assertEqual(status, LIVEACTION_STATUS_TIMED_OUT)
91
        self.assertTrue(output is not None)
92
        self.assertEqual(output['result'], 'None')
93
        self.assertEqual(output['error'], 'Action failed to complete in 0 seconds')
94
        self.assertEqual(output['exit_code'], -9)
95
96
    def test_simple_action_with_status_succeeded(self):
97
        runner = python_runner.get_runner()
98
        runner.action = self._get_mock_action_obj()
99
        runner.runner_parameters = {}
100
        runner.entry_point = PASCAL_ROW_ACTION_PATH
101
        runner.container_service = service.RunnerContainerService()
102
        runner.pre_run()
103
        (status, output, _) = runner.run({'row_index': 4})
104
        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
105
        self.assertTrue(output is not None)
106
        self.assertEqual(output['result'], [1, 4, 6, 4, 1])
107
108
    def test_simple_action_with_status_failed(self):
109
        runner = python_runner.get_runner()
110
        runner.action = self._get_mock_action_obj()
111
        runner.runner_parameters = {}
112
        runner.entry_point = PASCAL_ROW_ACTION_PATH
113
        runner.container_service = service.RunnerContainerService()
114
        runner.pre_run()
115
        (status, output, _) = runner.run({'row_index': 'a'})
116
        self.assertEqual(status, LIVEACTION_STATUS_FAILED)
117
        self.assertTrue(output is not None)
118
        self.assertEqual(output['result'], "This is suppose to fail don't worry!!")
119
120
    def test_simple_action_with_status_failed_result_none(self):
121
        runner = python_runner.get_runner()
122
        runner.action = self._get_mock_action_obj()
123
        runner.runner_parameters = {}
124
        runner.entry_point = PASCAL_ROW_ACTION_PATH
125
        runner.container_service = service.RunnerContainerService()
126
        runner.pre_run()
127
        (status, output, _) = runner.run({'row_index': 'c'})
128
        self.assertEqual(status, LIVEACTION_STATUS_FAILED)
129
        self.assertTrue(output is not None)
130
        self.assertEqual(output['result'], None)
131
132
    def test_exception_in_simple_action_with_invalid_status(self):
133
        runner = python_runner.get_runner()
134
        runner.action = self._get_mock_action_obj()
135
        runner.runner_parameters = {}
136
        runner.entry_point = PASCAL_ROW_ACTION_PATH
137
        runner.container_service = service.RunnerContainerService()
138
        runner.pre_run()
139
        self.assertRaises(ValueError,
140
                          runner.run, action_parameters={'row_index': 'd'})
141
142
    def test_simple_action_no_status_backward_compatibility(self):
143
        runner = python_runner.get_runner()
144
        runner.action = self._get_mock_action_obj()
145
        runner.runner_parameters = {}
146
        runner.entry_point = PASCAL_ROW_ACTION_PATH
147
        runner.container_service = service.RunnerContainerService()
148
        runner.pre_run()
149
        (status, output, _) = runner.run({'row_index': 'e'})
150
        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
151
        self.assertTrue(output is not None)
152
        self.assertEqual(output['result'], [1, 2])
153
154
    def test_simple_action_config_value_provided_overriden_in_datastore(self):
155
        wrapper = PythonActionWrapper(pack='dummy_pack_5', file_path=PASCAL_ROW_ACTION_PATH,
156
                                      user='joe')
157
158
        # No values provided in the datastore
159
        instance = wrapper._get_action_instance()
160
        self.assertEqual(instance.config['api_key'], 'some_api_key')  # static value
161
        self.assertEqual(instance.config['regions'], ['us-west-1'])  # static value
162
        self.assertEqual(instance.config['api_secret'], None)
163
        self.assertEqual(instance.config['private_key_path'], None)
164
165
        # api_secret overriden in the datastore (user scoped value)
166
        config_service.set_datastore_value_for_config_key(pack_name='dummy_pack_5',
167
                                                          key_name='api_secret',
168
                                                          user='joe',
169
                                                          value='foosecret',
170
                                                          secret=True)
171
172
        # private_key_path overriden in the datastore (global / non-user scoped value)
173
        config_service.set_datastore_value_for_config_key(pack_name='dummy_pack_5',
174
                                                          key_name='private_key_path',
175
                                                          value='foopath')
176
177
        instance = wrapper._get_action_instance()
178
        self.assertEqual(instance.config['api_key'], 'some_api_key')  # static value
179
        self.assertEqual(instance.config['regions'], ['us-west-1'])  # static value
180
        self.assertEqual(instance.config['api_secret'], 'foosecret')
181
        self.assertEqual(instance.config['private_key_path'], 'foopath')
182
183
    def test_simple_action_fail(self):
184
        runner = python_runner.get_runner()
185
        runner.action = self._get_mock_action_obj()
186
        runner.runner_parameters = {}
187
        runner.entry_point = PASCAL_ROW_ACTION_PATH
188
        runner.container_service = service.RunnerContainerService()
189
        runner.pre_run()
190
        (status, result, _) = runner.run({'row_index': '4'})
191
        self.assertTrue(result is not None)
192
        self.assertEqual(status, LIVEACTION_STATUS_FAILED)
193
194
    def test_simple_action_no_file(self):
195
        runner = python_runner.get_runner()
196
        runner.action = self._get_mock_action_obj()
197
        runner.runner_parameters = {}
198
        runner.entry_point = 'foo.py'
199
        runner.container_service = service.RunnerContainerService()
200
        runner.pre_run()
201
        (status, result, _) = runner.run({})
202
        self.assertTrue(result is not None)
203
        self.assertEqual(status, LIVEACTION_STATUS_FAILED)
204
205
    def test_simple_action_no_entry_point(self):
206
        runner = python_runner.get_runner()
207
        runner.action = self._get_mock_action_obj()
208
        runner.runner_parameters = {}
209
        runner.entry_point = ''
210
        runner.container_service = service.RunnerContainerService()
211
212
        expected_msg = 'Action .*? is missing entry_point attribute'
213
        self.assertRaisesRegexp(Exception, expected_msg, runner.run, {})
214
215
    @mock.patch('st2common.util.green.shell.subprocess.Popen')
216
    def test_action_with_user_supplied_env_vars(self, mock_popen):
217
        env_vars = {'key1': 'val1', 'key2': 'val2', 'PYTHONPATH': 'foobar'}
218
219
        mock_process = mock.Mock()
220
        mock_process.communicate.return_value = ('', '')
221
        mock_popen.return_value = mock_process
222
223
        runner = python_runner.get_runner()
224
        runner.action = self._get_mock_action_obj()
225
        runner.runner_parameters = {'env': env_vars}
226
        runner.entry_point = PASCAL_ROW_ACTION_PATH
227
        runner.container_service = service.RunnerContainerService()
228
        runner.pre_run()
229
        (_, _, _) = runner.run({'row_index': 4})
230
231
        _, call_kwargs = mock_popen.call_args
232
        actual_env = call_kwargs['env']
233
234
        for key, value in env_vars.items():
235
            # Verify that a blacklsited PYTHONPATH has been filtered out
236
            if key == 'PYTHONPATH':
237
                self.assertTrue(actual_env[key] != value)
238
            else:
239
                self.assertEqual(actual_env[key], value)
240
241
    @mock.patch('st2common.util.green.shell.subprocess.Popen')
242
    def test_stdout_interception_and_parsing(self, mock_popen):
243
        values = {'delimiter': ACTION_OUTPUT_RESULT_DELIMITER}
244
245
        # No output to stdout and no result (implicit None)
246
        mock_stdout = '%(delimiter)sNone%(delimiter)s' % values
247
        mock_stderr = 'foo stderr'
248
        mock_process = mock.Mock()
249
        mock_process.communicate.return_value = (mock_stdout, mock_stderr)
250
        mock_process.returncode = 0
251
        mock_popen.return_value = mock_process
252
253
        runner = python_runner.get_runner()
254
        runner.action = self._get_mock_action_obj()
255
        runner.runner_parameters = {}
256
        runner.entry_point = PASCAL_ROW_ACTION_PATH
257
        runner.container_service = service.RunnerContainerService()
258
        runner.pre_run()
259
        (_, output, _) = runner.run({'row_index': 4})
260
261
        self.assertEqual(output['stdout'], '')
262
        self.assertEqual(output['stderr'], mock_stderr)
263
        self.assertEqual(output['result'], 'None')
264
        self.assertEqual(output['exit_code'], 0)
265
266
        # Output to stdout, no result (implicit None),return_code 1 and status
267
        # failed
268
        mock_stdout = 'pre result%(delimiter)sNone%(delimiter)spost result' % values
269
        mock_stderr = 'foo stderr'
270
        mock_process = mock.Mock()
271
        mock_process.communicate.return_value = (mock_stdout, mock_stderr)
272
        mock_process.returncode = 1
273
        mock_popen.return_value = mock_process
274
275
        runner = python_runner.get_runner()
276
        runner.action = self._get_mock_action_obj()
277
        runner.runner_parameters = {}
278
        runner.entry_point = PASCAL_ROW_ACTION_PATH
279
        runner.container_service = service.RunnerContainerService()
280
        runner.pre_run()
281
        (status, output, _) = runner.run({'row_index': 4})
282
        self.assertEqual(output['stdout'], 'pre resultpost result')
283
        self.assertEqual(output['stderr'], mock_stderr)
284
        self.assertEqual(output['result'], 'None')
285
        self.assertEqual(output['exit_code'], 1)
286
        self.assertEqual(status, 'failed')
287
288
        # Output to stdout, no result (implicit None), return_code 1 and status
289
        # succedded
290
        mock_stdout = 'pre result%(delimiter)sNone%(delimiter)spost result' % values
291
        mock_stderr = 'foo stderr'
292
        mock_process = mock.Mock()
293
        mock_process.communicate.return_value = (mock_stdout, mock_stderr)
294
        mock_process.returncode = 0
295
        mock_popen.return_value = mock_process
296
        runner = python_runner.get_runner()
297
        runner.action = self._get_mock_action_obj()
298
        runner.runner_parameters = {}
299
        runner.entry_point = PASCAL_ROW_ACTION_PATH
300
        runner.container_service = service.RunnerContainerService()
301
        runner.pre_run()
302
        (status, output, _) = runner.run({'row_index': 4})
303
        self.assertEqual(output['stdout'], 'pre resultpost result')
304
        self.assertEqual(output['stderr'], mock_stderr)
305
        self.assertEqual(output['result'], 'None')
306
        self.assertEqual(output['exit_code'], 0)
307
        self.assertEqual(status, 'succeeded')
308
309
    @mock.patch('st2common.util.green.shell.subprocess.Popen')
310
    def test_common_st2_env_vars_are_available_to_the_action(self, mock_popen):
311
        mock_process = mock.Mock()
312
        mock_process.communicate.return_value = ('', '')
313
        mock_popen.return_value = mock_process
314
315
        runner = python_runner.get_runner()
316
        runner.auth_token = mock.Mock()
317
        runner.auth_token.token = 'ponies'
318
        runner.action = self._get_mock_action_obj()
319
        runner.runner_parameters = {}
320
        runner.entry_point = PASCAL_ROW_ACTION_PATH
321
        runner.container_service = service.RunnerContainerService()
322
        runner.pre_run()
323
        (_, _, _) = runner.run({'row_index': 4})
324
325
        _, call_kwargs = mock_popen.call_args
326
        actual_env = call_kwargs['env']
327
        self.assertCommonSt2EnvVarsAvailableInEnv(env=actual_env)
328
329
    def test_action_class_instantiation_action_service_argument(self):
330
        class Action1(Action):
331
            # Constructor not overriden so no issue here
332
            pass
333
334
            def run(self):
335
                pass
336
337
        class Action2(Action):
338
            # Constructor overriden, but takes action_service argument
339
            def __init__(self, config, action_service=None):
340
                super(Action2, self).__init__(config=config,
341
                                              action_service=action_service)
342
343
            def run(self):
344
                pass
345
346
        class Action3(Action):
347
            # Constructor overriden, but doesn't take to action service
348
            def __init__(self, config):
349
                super(Action3, self).__init__(config=config)
350
351
            def run(self):
352
                pass
353
354
        config = {'a': 1, 'b': 2}
355
        action_service = 'ActionService!'
356
357
        action1 = get_action_class_instance(action_cls=Action1, config=config,
358
                                            action_service=action_service)
359
        self.assertEqual(action1.config, config)
360
        self.assertEqual(action1.action_service, action_service)
361
362
        action2 = get_action_class_instance(action_cls=Action2, config=config,
363
                                            action_service=action_service)
364
        self.assertEqual(action2.config, config)
365
        self.assertEqual(action2.action_service, action_service)
366
367
        action3 = get_action_class_instance(action_cls=Action3, config=config,
368
                                            action_service=action_service)
369
        self.assertEqual(action3.config, config)
370
        self.assertEqual(action3.action_service, action_service)
371
372
    def test_action_with_same_module_name_as_module_in_stdlib(self):
373
        runner = python_runner.get_runner()
374
        runner.action = self._get_mock_action_obj()
375
        runner.runner_parameters = {}
376
        runner.entry_point = TEST_ACTION_PATH
377
        runner.container_service = service.RunnerContainerService()
378
        runner.pre_run()
379
        (status, output, _) = runner.run({})
380
        self.assertEqual(status, LIVEACTION_STATUS_SUCCEEDED)
381
        self.assertTrue(output is not None)
382
        self.assertEqual(output['result'], 'test action')
383
384
    def _get_mock_action_obj(self):
385
        """
386
        Return mock action object.
387
388
        Pack gets set to the system pack so the action doesn't require a separate virtualenv.
389
        """
390
        action = mock.Mock()
391
        action.pack = SYSTEM_PACK_NAME
392
        action.entry_point = 'foo.py'
393
        return action
394