Completed
Push — master ( d1f0a7...3b2ece )
by Edward
21:04 queued 05:38
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

3 Methods

Rating   Name   Duplication   Size   Complexity  
A st2tests.EventletTestCase.setUpClass() 0 8 2
A st2tests.EventletTestCase.tearDownClass() 0 8 1
A st2tests.BaseTestCase._register_packs() 0 7 1
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.sensor as sensor_model
39
import st2common.models.db.trigger as trigger_model
40
import st2common.models.db.action as action_model
41
import st2common.models.db.keyvalue as keyvalue_model
42
import st2common.models.db.runner as runner_model
43
import st2common.models.db.execution as execution_model
44
import st2common.models.db.executionstate as executionstate_model
45
import st2common.models.db.liveaction as liveaction_model
46
import st2common.models.db.actionalias as actionalias_model
47
import st2common.models.db.policy as policy_model
48
49
50
import st2tests.config
51
52
53
__all__ = [
54
    'EventletTestCase',
55
    'DbTestCase',
56
    'DbModelTestCase',
57
    'CleanDbTestCase',
58
    'CleanFilesTestCase',
59
    'IntegrationTestCase'
60
]
61
62
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
63
64
ALL_MODELS = []
65
ALL_MODELS.extend(rule_model.MODELS)
66
ALL_MODELS.extend(sensor_model.MODELS)
67
ALL_MODELS.extend(trigger_model.MODELS)
68
ALL_MODELS.extend(action_model.MODELS)
69
ALL_MODELS.extend(keyvalue_model.MODELS)
70
ALL_MODELS.extend(runner_model.MODELS)
71
ALL_MODELS.extend(execution_model.MODELS)
72
ALL_MODELS.extend(executionstate_model.MODELS)
73
ALL_MODELS.extend(liveaction_model.MODELS)
74
ALL_MODELS.extend(actionalias_model.MODELS)
75
ALL_MODELS.extend(policy_model.MODELS)
76
77
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
78
TESTS_CONFIG_PATH = os.path.join(BASE_DIR, '../conf/st2.conf')
79
80
81
class BaseTestCase(TestCase):
82
83
    @classmethod
84
    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...
85
        """
86
        Register all the packs inside the fixtures directory.
87
        """
88
        registrar = ResourceRegistrar(use_pack_cache=False)
89
        registrar.register_packs(base_dirs=get_packs_base_paths())
90
91
92
class EventletTestCase(TestCase):
93
    """
94
    Base test class which performs eventlet monkey patching before the tests run
95
    and un-patching after the tests have finished running.
96
    """
97
98
    @classmethod
99
    def setUpClass(cls):
100
        eventlet.monkey_patch(
101
            os=True,
102
            select=True,
103
            socket=True,
104
            thread=False if '--use-debugger' in sys.argv else True,
105
            time=True
106
        )
107
108
    @classmethod
109
    def tearDownClass(cls):
110
        eventlet.monkey_patch(
111
            os=False,
112
            select=False,
113
            socket=False,
114
            thread=False,
115
            time=False
116
        )
117
118
119
class BaseDbTestCase(BaseTestCase):
120
121
    # Set to True to enable printing of all the log messages to the console
122
    DISPLAY_LOG_MESSAGES = False
123
124
    @classmethod
125
    def setUpClass(cls):
126
        st2tests.config.parse_args()
127
128
        if cls.DISPLAY_LOG_MESSAGES:
129
            config_path = os.path.join(BASE_DIR, '../conf/logging.conf')
130
            logging.config.fileConfig(config_path,
131
                                      disable_existing_loggers=False)
132
133
    @classmethod
134
    def _establish_connection_and_re_create_db(cls):
135
        username = cfg.CONF.database.username if hasattr(cfg.CONF.database, 'username') else None
136
        password = cfg.CONF.database.password if hasattr(cfg.CONF.database, 'password') else None
137
        cls.db_connection = db_setup(
138
            cfg.CONF.database.db_name, cfg.CONF.database.host, cfg.CONF.database.port,
139
            username=username, password=password, ensure_indexes=False)
140
        cls._drop_collections()
141
        cls.db_connection.drop_database(cfg.CONF.database.db_name)
142
143
        # Explicity ensure indexes after we re-create the DB otherwise ensure_indexes could failure
144
        # inside db_setup if test inserted invalid data
145
        db_ensure_indexes()
146
147
    @classmethod
148
    def _drop_db(cls):
149
        cls._drop_collections()
150
        if cls.db_connection is not None:
151
            cls.db_connection.drop_database(cfg.CONF.database.db_name)
152
        db_teardown()
153
        cls.db_connection = None
154
155
    @classmethod
156
    def _drop_collections(cls):
157
        # XXX: Explicitly drop all the collection. Otherwise, artifacts are left over in
158
        # subsequent tests.
159
        # See: https://github.com/MongoEngine/mongoengine/issues/566
160
        # See: https://github.com/MongoEngine/mongoengine/issues/565
161
        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...
162
        for model in ALL_MODELS:
163
            model.drop_collection()
164
165
166
class DbTestCase(BaseDbTestCase):
167
    """
168
    This class drops and re-creates the database once per TestCase run.
169
170
    This means database is only dropped once before all the tests from this class run. This means
171
    data is persited between different tests in this class.
172
    """
173
174
    db_connection = None
175
    current_result = None
176
    register_packs = False
177
178
    @classmethod
179
    def setUpClass(cls):
180
        BaseDbTestCase.setUpClass()
181
        cls._establish_connection_and_re_create_db()
182
183
        if cls.register_packs:
184
            cls._register_packs()
185
186
    @classmethod
187
    def tearDownClass(cls):
188
        drop_db = True
189
190
        if cls.current_result.errors or cls.current_result.failures:
191
            # Don't drop DB on test failure
192
            drop_db = False
193
194
        if drop_db:
195
            cls._drop_db()
196
197
    def run(self, result=None):
198
        # Remember result for use in tearDown and tearDownClass
199
        self.current_result = result
200
        self.__class__.current_result = result
201
        super(DbTestCase, self).run(result=result)
202
203
204
class DbModelTestCase(DbTestCase):
205
    access_type = None
206
207
    @classmethod
208
    def setUpClass(cls):
209
        super(DbModelTestCase, cls).setUpClass()
210
        cls.db_type = cls.access_type.impl.model
211
212
    def _assert_fields_equal(self, a, b, exclude=None):
213
        exclude = exclude or []
214
        fields = {k: v for k, v in six.iteritems(self.db_type._fields) if k not in exclude}
215
216
        assert_funcs = {
217
            'mongoengine.fields.DictField': self.assertDictEqual,
218
            'mongoengine.fields.ListField': self.assertListEqual,
219
            'mongoengine.fields.SortedListField': self.assertListEqual
220
        }
221
222
        for k, v in six.iteritems(fields):
223
            assert_func = assert_funcs.get(str(v), self.assertEqual)
224
            assert_func(getattr(a, k, None), getattr(b, k, None))
225
226
    def _assert_values_equal(self, a, values=None):
227
        values = values or {}
228
229
        assert_funcs = {
230
            'dict': self.assertDictEqual,
231
            'list': self.assertListEqual
232
        }
233
234
        for k, v in six.iteritems(values):
235
            assert_func = assert_funcs.get(type(v).__name__, self.assertEqual)
236
            assert_func(getattr(a, k, None), v)
237
238
    def _assert_crud(self, instance, defaults=None, updates=None):
239
        # Assert instance is not already in the database.
240
        self.assertIsNone(getattr(instance, 'id', None))
241
242
        # Assert default values are assigned.
243
        self._assert_values_equal(instance, values=defaults)
244
245
        # Assert instance is created in the datbaase.
246
        saved = self.access_type.add_or_update(instance)
247
        self.assertIsNotNone(saved.id)
248
        self._assert_fields_equal(instance, saved, exclude=['id'])
249
        retrieved = self.access_type.get_by_id(saved.id)
250
        self._assert_fields_equal(saved, retrieved)
251
252
        # Assert instance is updated in the database.
253
        for k, v in six.iteritems(updates or {}):
254
            setattr(instance, k, v)
255
256
        updated = self.access_type.add_or_update(instance)
257
        self._assert_fields_equal(instance, updated)
258
259
        # Assert instance is deleted from the database.
260
        retrieved = self.access_type.get_by_id(instance.id)
261
        retrieved.delete()
262
        self.assertRaises(ValueError, self.access_type.get_by_id, instance.id)
263
264
    def _assert_unique_key_constraint(self, instance):
265
        # Assert instance is not already in the database.
266
        self.assertIsNone(getattr(instance, 'id', None))
267
268
        # Assert instance is created in the datbaase.
269
        saved = self.access_type.add_or_update(instance)
270
        self.assertIsNotNone(saved.id)
271
272
        # Assert exception is thrown if try to create same instance again.
273
        delattr(instance, 'id')
274
        self.assertRaises(StackStormDBObjectConflictError,
275
                          self.access_type.add_or_update,
276
                          instance)
277
278
279
class CleanDbTestCase(BaseDbTestCase):
280
    """
281
    Class which ensures database is re-created before running each test method.
282
283
    This means each test inside this class is self-sustained and starts with a clean (empty)
284
    database.
285
    """
286
287
    def setUp(self):
288
        self._establish_connection_and_re_create_db()
289
290
291
class CleanFilesTestCase(TestCase):
292
    """
293
    Base test class which deletes specified files and directories on setUp and `tearDown.
294
    """
295
    to_delete_files = []
296
    to_delete_directories = []
297
298
    def setUp(self):
299
        super(CleanFilesTestCase, self).setUp()
300
        self._delete_files()
301
302
    def tearDown(self):
303
        super(CleanFilesTestCase, self).tearDown()
304
        self._delete_files()
305
306
    def _delete_files(self):
307
        for file_path in self.to_delete_files:
308
            if not os.path.isfile(file_path):
309
                continue
310
311
            try:
312
                os.remove(file_path)
313
            except Exception:
314
                pass
315
316
        for file_path in self.to_delete_directories:
317
            if not os.path.isdir(file_path):
318
                continue
319
320
            try:
321
                shutil.rmtree(file_path)
322
            except Exception:
323
                pass
324
325
326
class IntegrationTestCase(TestCase):
327
    """
328
    Base test class for integration tests to inherit from.
329
330
    It includes various utility functions and assert methods for working with processes.
331
    """
332
333
    # Set to True to print process stdout and stderr in tearDown after killing the processes
334
    # which are still alive
335
    print_stdout_stderr_on_teardown = False
336
337
    processes = {}
338
339
    def tearDown(self):
340
        super(IntegrationTestCase, self).tearDown()
341
342
        # Make sure we kill all the processes on teardown so they don't linger around if an
343
        # exception was thrown.
344
        for pid, process in self.processes.items():
345
346
            try:
347
                process.kill()
348
            except OSError:
349
                # Process already exited or similar
350
                pass
351
352
            if self.print_stdout_stderr_on_teardown:
353
                try:
354
                    stdout = process.stdout.read()
355
                except:
356
                    stdout = None
357
358
                try:
359
                    stderr = process.stderr.read()
360
                except:
361
                    stderr = None
362
363
                print('Process "%s"' % (process.pid))
364
                print('Stdout: %s' % (stdout))
365
                print('Stderr: %s' % (stderr))
366
367
    def add_process(self, process):
368
        """
369
        Add a process to the local data structure to make sure it will get killed and cleaned up on
370
        tearDown.
371
        """
372
        self.processes[process.pid] = process
373
374
    def remove_process(self, process):
375
        """
376
        Remove process from a local data structure.
377
        """
378
        if process.pid in self.processes:
379
            del self.processes[process.pid]
380
381
    def assertProcessIsRunning(self, process):
382
        """
383
        Assert that a long running process provided Process object as returned by subprocess.Popen
384
        has succesfuly started and is running.
385
        """
386
        return_code = process.poll()
387
388
        if return_code is not None:
389
            stdout = process.stdout.read()
390
            stderr = process.stderr.read()
391
            msg = ('Process exited with code=%s.\nStdout:\n%s\n\nStderr:\n%s' %
392
                   (return_code, stdout, stderr))
393
            self.fail(msg)
394
395
    def assertProcessExited(self, proc):
396
        try:
397
            status = proc.status()
398
        except psutil.NoSuchProcess:
399
            status = 'exited'
400
401
        if status not in ['exited', 'zombie']:
402
            self.fail('Process with pid "%s" is still running' % (proc.pid))
403
404
405
class FakeResponse(object):
406
407
    def __init__(self, text, status_code, reason):
408
        self.text = text
409
        self.status_code = status_code
410
        self.reason = reason
411
412
    def json(self):
413
        return json.loads(self.text)
414
415
    def raise_for_status(self):
416
        raise Exception(self.reason)
417
418
419
def get_fixtures_path():
420
    return os.path.join(os.path.dirname(__file__), 'fixtures')
421
422
423
def get_resources_path():
424
    return os.path.join(os.path.dirname(__file__), 'resources')
425