Completed
Pull Request — master (#2304)
by Arma
07:07
created

st2tests.ExamplesTest   A

Complexity

Total Complexity 19

Size/Duplication

Total Lines 105
Duplicated Lines 0 %
Metric Value
wmc 19
dl 0
loc 105
rs 10

10 Methods

Rating   Name   Duplication   Size   Complexity  
A test_handle_retry() 0 4 1
A test_handle_error_task_default() 0 9 2
B test_branching() 0 27 4
A test_repeat_with_items() 0 7 1
A test_handle_error() 0 9 2
A test_workbook_multiple_subflows() 0 4 1
A test_with_items_batch_processing() 0 13 2
A test_repeat() 0 7 1
A test_environment() 0 7 2
A test_join() 0 7 3
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
try:
17
    import simplejson as json
18
except ImportError:
19
    import json
20
21
import os
22
import os.path
23
import sys
24
import shutil
25
import logging
26
27
import six
28
import eventlet
29
import psutil
30
from oslo_config import cfg
31
from unittest2 import TestCase
32
33
from st2common.exceptions.db import StackStormDBObjectConflictError
34
from st2common.models.db import db_setup, db_teardown, db_ensure_indexes
35
from st2common.bootstrap.base import ResourceRegistrar
36
from st2common.content.utils import get_packs_base_paths
37
import st2common.models.db.rule as rule_model
38
import st2common.models.db.rule_enforcement as rule_enforcement_model
39
import st2common.models.db.sensor as sensor_model
40
import st2common.models.db.trigger as trigger_model
41
import st2common.models.db.action as action_model
42
import st2common.models.db.keyvalue as keyvalue_model
43
import st2common.models.db.runner as runner_model
44
import st2common.models.db.execution as execution_model
45
import st2common.models.db.executionstate as executionstate_model
46
import st2common.models.db.liveaction as liveaction_model
47
import st2common.models.db.actionalias as actionalias_model
48
import st2common.models.db.policy as policy_model
49
50
import st2tests.config
51
from st2tests.mocks.sensor import MockSensorWrapper
52
from st2tests.mocks.sensor import MockSensorService
53
54
55
__all__ = [
56
    'EventletTestCase',
57
    'DbTestCase',
58
    'DbModelTestCase',
59
    'CleanDbTestCase',
60
    'CleanFilesTestCase',
61
    'IntegrationTestCase',
62
63
    'BaseSensorTestCase',
64
    'BaseActionTestCase'
65
]
66
67
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
68
69
ALL_MODELS = []
70
ALL_MODELS.extend(rule_model.MODELS)
71
ALL_MODELS.extend(sensor_model.MODELS)
72
ALL_MODELS.extend(trigger_model.MODELS)
73
ALL_MODELS.extend(action_model.MODELS)
74
ALL_MODELS.extend(keyvalue_model.MODELS)
75
ALL_MODELS.extend(runner_model.MODELS)
76
ALL_MODELS.extend(execution_model.MODELS)
77
ALL_MODELS.extend(executionstate_model.MODELS)
78
ALL_MODELS.extend(liveaction_model.MODELS)
79
ALL_MODELS.extend(actionalias_model.MODELS)
80
ALL_MODELS.extend(policy_model.MODELS)
81
ALL_MODELS.extend(rule_enforcement_model.MODELS)
82
83
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
84
TESTS_CONFIG_PATH = os.path.join(BASE_DIR, '../conf/st2.conf')
85
86
87
class BaseTestCase(TestCase):
88
89
    @classmethod
90
    def _register_packs(self):
0 ignored issues
show
Coding Style Best Practice introduced by
The first argument of the class method _register_packs should be named cls.
Loading history...
91
        """
92
        Register all the packs inside the fixtures directory.
93
        """
94
        registrar = ResourceRegistrar(use_pack_cache=False)
95
        registrar.register_packs(base_dirs=get_packs_base_paths())
96
97
98
class EventletTestCase(TestCase):
99
    """
100
    Base test class which performs eventlet monkey patching before the tests run
101
    and un-patching after the tests have finished running.
102
    """
103
104
    @classmethod
105
    def setUpClass(cls):
106
        eventlet.monkey_patch(
107
            os=True,
108
            select=True,
109
            socket=True,
110
            thread=False if '--use-debugger' in sys.argv else True,
111
            time=True
112
        )
113
114
    @classmethod
115
    def tearDownClass(cls):
116
        eventlet.monkey_patch(
117
            os=False,
118
            select=False,
119
            socket=False,
120
            thread=False,
121
            time=False
122
        )
123
124
125
class BaseDbTestCase(BaseTestCase):
126
127
    # Set to True to enable printing of all the log messages to the console
128
    DISPLAY_LOG_MESSAGES = False
129
130
    @classmethod
131
    def setUpClass(cls):
132
        st2tests.config.parse_args()
133
134
        if cls.DISPLAY_LOG_MESSAGES:
135
            config_path = os.path.join(BASE_DIR, '../conf/logging.conf')
136
            logging.config.fileConfig(config_path,
137
                                      disable_existing_loggers=False)
138
139
    @classmethod
140
    def _establish_connection_and_re_create_db(cls):
141
        username = cfg.CONF.database.username if hasattr(cfg.CONF.database, 'username') else None
142
        password = cfg.CONF.database.password if hasattr(cfg.CONF.database, 'password') else None
143
        cls.db_connection = db_setup(
144
            cfg.CONF.database.db_name, cfg.CONF.database.host, cfg.CONF.database.port,
145
            username=username, password=password, ensure_indexes=False)
146
        cls._drop_collections()
147
        cls.db_connection.drop_database(cfg.CONF.database.db_name)
148
149
        # Explicity ensure indexes after we re-create the DB otherwise ensure_indexes could failure
150
        # inside db_setup if test inserted invalid data
151
        db_ensure_indexes()
152
153
    @classmethod
154
    def _drop_db(cls):
155
        cls._drop_collections()
156
        if cls.db_connection is not None:
157
            cls.db_connection.drop_database(cfg.CONF.database.db_name)
158
        db_teardown()
159
        cls.db_connection = None
160
161
    @classmethod
162
    def _drop_collections(cls):
163
        # XXX: Explicitly drop all the collection. Otherwise, artifacts are left over in
164
        # subsequent tests.
165
        # See: https://github.com/MongoEngine/mongoengine/issues/566
166
        # See: https://github.com/MongoEngine/mongoengine/issues/565
167
        global ALL_MODELS
0 ignored issues
show
Unused Code introduced by
The variable ALL_MODELS was imported from global scope, but was never written to.
Loading history...
168
        for model in ALL_MODELS:
169
            model.drop_collection()
170
171
172
class DbTestCase(BaseDbTestCase):
173
    """
174
    This class drops and re-creates the database once per TestCase run.
175
176
    This means database is only dropped once before all the tests from this class run. This means
177
    data is persited between different tests in this class.
178
    """
179
180
    db_connection = None
181
    current_result = None
182
    register_packs = False
183
184
    @classmethod
185
    def setUpClass(cls):
186
        BaseDbTestCase.setUpClass()
187
        cls._establish_connection_and_re_create_db()
188
189
        if cls.register_packs:
190
            cls._register_packs()
191
192
    @classmethod
193
    def tearDownClass(cls):
194
        drop_db = True
195
196
        if cls.current_result.errors or cls.current_result.failures:
197
            # Don't drop DB on test failure
198
            drop_db = False
199
200
        if drop_db:
201
            cls._drop_db()
202
203
    def run(self, result=None):
204
        # Remember result for use in tearDown and tearDownClass
205
        self.current_result = result
206
        self.__class__.current_result = result
207
        super(DbTestCase, self).run(result=result)
208
209
210
class DbModelTestCase(DbTestCase):
211
    access_type = None
212
213
    @classmethod
214
    def setUpClass(cls):
215
        super(DbModelTestCase, cls).setUpClass()
216
        cls.db_type = cls.access_type.impl.model
217
218
    def _assert_fields_equal(self, a, b, exclude=None):
219
        exclude = exclude or []
220
        fields = {k: v for k, v in six.iteritems(self.db_type._fields) if k not in exclude}
221
222
        assert_funcs = {
223
            'mongoengine.fields.DictField': self.assertDictEqual,
224
            'mongoengine.fields.ListField': self.assertListEqual,
225
            'mongoengine.fields.SortedListField': self.assertListEqual
226
        }
227
228
        for k, v in six.iteritems(fields):
229
            assert_func = assert_funcs.get(str(v), self.assertEqual)
230
            assert_func(getattr(a, k, None), getattr(b, k, None))
231
232
    def _assert_values_equal(self, a, values=None):
233
        values = values or {}
234
235
        assert_funcs = {
236
            'dict': self.assertDictEqual,
237
            'list': self.assertListEqual
238
        }
239
240
        for k, v in six.iteritems(values):
241
            assert_func = assert_funcs.get(type(v).__name__, self.assertEqual)
242
            assert_func(getattr(a, k, None), v)
243
244
    def _assert_crud(self, instance, defaults=None, updates=None):
245
        # Assert instance is not already in the database.
246
        self.assertIsNone(getattr(instance, 'id', None))
247
248
        # Assert default values are assigned.
249
        self._assert_values_equal(instance, values=defaults)
250
251
        # Assert instance is created in the datbaase.
252
        saved = self.access_type.add_or_update(instance)
253
        self.assertIsNotNone(saved.id)
254
        self._assert_fields_equal(instance, saved, exclude=['id'])
255
        retrieved = self.access_type.get_by_id(saved.id)
256
        self._assert_fields_equal(saved, retrieved)
257
258
        # Assert instance is updated in the database.
259
        for k, v in six.iteritems(updates or {}):
260
            setattr(instance, k, v)
261
262
        updated = self.access_type.add_or_update(instance)
263
        self._assert_fields_equal(instance, updated)
264
265
        # Assert instance is deleted from the database.
266
        retrieved = self.access_type.get_by_id(instance.id)
267
        retrieved.delete()
268
        self.assertRaises(ValueError, self.access_type.get_by_id, instance.id)
269
270
    def _assert_unique_key_constraint(self, instance):
271
        # Assert instance is not already in the database.
272
        self.assertIsNone(getattr(instance, 'id', None))
273
274
        # Assert instance is created in the datbaase.
275
        saved = self.access_type.add_or_update(instance)
276
        self.assertIsNotNone(saved.id)
277
278
        # Assert exception is thrown if try to create same instance again.
279
        delattr(instance, 'id')
280
        self.assertRaises(StackStormDBObjectConflictError,
281
                          self.access_type.add_or_update,
282
                          instance)
283
284
285
class CleanDbTestCase(BaseDbTestCase):
286
    """
287
    Class which ensures database is re-created before running each test method.
288
289
    This means each test inside this class is self-sustained and starts with a clean (empty)
290
    database.
291
    """
292
293
    def setUp(self):
294
        self._establish_connection_and_re_create_db()
295
296
297
class CleanFilesTestCase(TestCase):
298
    """
299
    Base test class which deletes specified files and directories on setUp and `tearDown.
300
    """
301
    to_delete_files = []
302
    to_delete_directories = []
303
304
    def setUp(self):
305
        super(CleanFilesTestCase, self).setUp()
306
        self._delete_files()
307
308
    def tearDown(self):
309
        super(CleanFilesTestCase, self).tearDown()
310
        self._delete_files()
311
312
    def _delete_files(self):
313
        for file_path in self.to_delete_files:
314
            if not os.path.isfile(file_path):
315
                continue
316
317
            try:
318
                os.remove(file_path)
319
            except Exception:
320
                pass
321
322
        for file_path in self.to_delete_directories:
323
            if not os.path.isdir(file_path):
324
                continue
325
326
            try:
327
                shutil.rmtree(file_path)
328
            except Exception:
329
                pass
330
331
332
class IntegrationTestCase(TestCase):
333
    """
334
    Base test class for integration tests to inherit from.
335
336
    It includes various utility functions and assert methods for working with processes.
337
    """
338
339
    # Set to True to print process stdout and stderr in tearDown after killing the processes
340
    # which are still alive
341
    print_stdout_stderr_on_teardown = False
342
343
    processes = {}
344
345
    def tearDown(self):
346
        super(IntegrationTestCase, self).tearDown()
347
348
        # Make sure we kill all the processes on teardown so they don't linger around if an
349
        # exception was thrown.
350
        for pid, process in self.processes.items():
351
352
            try:
353
                process.kill()
354
            except OSError:
355
                # Process already exited or similar
356
                pass
357
358
            if self.print_stdout_stderr_on_teardown:
359
                try:
360
                    stdout = process.stdout.read()
361
                except:
362
                    stdout = None
363
364
                try:
365
                    stderr = process.stderr.read()
366
                except:
367
                    stderr = None
368
369
                print('Process "%s"' % (process.pid))
370
                print('Stdout: %s' % (stdout))
371
                print('Stderr: %s' % (stderr))
372
373
    def add_process(self, process):
374
        """
375
        Add a process to the local data structure to make sure it will get killed and cleaned up on
376
        tearDown.
377
        """
378
        self.processes[process.pid] = process
379
380
    def remove_process(self, process):
381
        """
382
        Remove process from a local data structure.
383
        """
384
        if process.pid in self.processes:
385
            del self.processes[process.pid]
386
387
    def assertProcessIsRunning(self, process):
388
        """
389
        Assert that a long running process provided Process object as returned by subprocess.Popen
390
        has succesfuly started and is running.
391
        """
392
        return_code = process.poll()
393
394
        if return_code is not None:
395
            stdout = process.stdout.read()
396
            stderr = process.stderr.read()
397
            msg = ('Process exited with code=%s.\nStdout:\n%s\n\nStderr:\n%s' %
398
                   (return_code, stdout, stderr))
399
            self.fail(msg)
400
401
    def assertProcessExited(self, proc):
402
        try:
403
            status = proc.status()
404
        except psutil.NoSuchProcess:
405
            status = 'exited'
406
407
        if status not in ['exited', 'zombie']:
408
            self.fail('Process with pid "%s" is still running' % (proc.pid))
409
410
411
class BaseSensorTestCase(TestCase):
412
    """
413
    Base class for sensor tests.
414
415
    This class provides some utility methods for verifying that a trigger has
416
    been dispatched, etc.
417
    """
418
419
    sensor_cls = None
420
421
    def setUp(self):
422
        super(BaseSensorTestCase, self).setUp()
423
424
        class_name = self.sensor_cls.__name__
425
        sensor_wrapper = MockSensorWrapper(pack='tests', class_name=class_name)
426
        self.sensor_service = MockSensorService(sensor_wrapper=sensor_wrapper)
427
428
    def get_sensor_instance(self, config=None, poll_interval=None):
429
        """
430
        Retrieve instance of the sensor class.
431
        """
432
        kwargs = {
433
            'sensor_service': self.sensor_service
434
        }
435
436
        if config:
437
            kwargs['config'] = config
438
439
        if poll_interval is not None:
440
            kwargs['poll_interval'] = poll_interval
441
442
        instance = self.sensor_cls(**kwargs)  # pylint: disable=not-callable
443
        return instance
444
445
    def get_dispatched_triggers(self):
446
        return self.sensor_service.dispatched_triggers
447
448
    def get_last_dispatched_trigger(self):
449
        return self.sensor_service.dispatched_triggers[-1]
450
451
    def assertTriggerDispatched(self, trigger, payload=None, trace_context=None):
452
        """
453
        Assert that the trigger with the provided values has been dispatched.
454
455
        :param trigger: Name of the trigger.
456
        :type trigger: ``str``
457
458
        :param paylod: Trigger payload (optional). If not provided, only trigger name is matched.
459
        type: payload: ``object``
460
461
        :param trace_context: Trigger trace context (optional). If not provided, only trigger name
462
                              is matched.
463
        type: payload: ``object``
464
        """
465
        dispatched_triggers = self.get_dispatched_triggers()
466
        for item in dispatched_triggers:
467
            trigger_matches = (item['trigger'] == trigger)
468
469
            if payload:
470
                payload_matches = (item['payload'] == payload)
471
            else:
472
                payload_matches = True
473
474
            if trace_context:
475
                trace_context_matches = (item['trace_context'] == trace_context)
476
            else:
477
                trace_context_matches = True
478
479
            if trigger_matches and payload_matches and trace_context_matches:
480
                return True
481
482
        msg = 'Trigger "%s" hasn\'t been dispatched' % (trigger)
483
        raise AssertionError(msg)
484
485
486
class BaseActionTestCase(TestCase):
487
    """
488
    Base class for action tests.
489
    """
490
491
    sensor_cls = None
492
493
494
class FakeResponse(object):
495
496
    def __init__(self, text, status_code, reason):
497
        self.text = text
498
        self.status_code = status_code
499
        self.reason = reason
500
501
    def json(self):
502
        return json.loads(self.text)
503
504
    def raise_for_status(self):
505
        raise Exception(self.reason)
506
507
508
def get_fixtures_path():
509
    return os.path.join(os.path.dirname(__file__), 'fixtures')
510
511
512
def get_resources_path():
513
    return os.path.join(os.path.dirname(__file__), 'resources')
514