PageRenderTime 41ms CodeModel.GetById 27ms app.highlight 12ms RepoModel.GetById 0ms app.codeStats 0ms

/common/djangoapps/util/tests/test_db.py

https://gitlab.com/unofficial-mirrors/edx-platform
Python | 239 lines | 225 code | 7 blank | 7 comment | 0 complexity | 03d9cccba0cca57c2c0963a3f8ad2058 MD5 | raw file
  1"""Tests for util.db module."""
  2
  3import threading
  4import time
  5import unittest
  6
  7import ddt
  8from django.conf import settings
  9from django.contrib.auth.models import User
 10from django.core.management import call_command
 11from django.db import IntegrityError, connection
 12from django.db.transaction import TransactionManagementError, atomic
 13from django.test import TestCase, TransactionTestCase
 14from django.test.utils import override_settings
 15from django.utils.six import StringIO
 16
 17from util.db import NoOpMigrationModules, commit_on_success, enable_named_outer_atomic, generate_int_id, outer_atomic
 18
 19
 20def do_nothing():
 21    """Just return."""
 22    return
 23
 24
 25@ddt.ddt
 26class TransactionManagersTestCase(TransactionTestCase):
 27    """
 28    Tests commit_on_success and outer_atomic.
 29
 30    Note: This TestCase only works with MySQL.
 31
 32    To test do: "./manage.py lms --settings=test_with_mysql test util.tests.test_db"
 33    """
 34    DECORATORS = {
 35        'outer_atomic': outer_atomic(),
 36        'outer_atomic_read_committed': outer_atomic(read_committed=True),
 37        'commit_on_success': commit_on_success(),
 38        'commit_on_success_read_committed': commit_on_success(read_committed=True),
 39    }
 40
 41    @ddt.data(
 42        ('outer_atomic', IntegrityError, None, True),
 43        ('outer_atomic_read_committed', type(None), False, True),
 44        ('commit_on_success', IntegrityError, None, True),
 45        ('commit_on_success_read_committed', type(None), False, True),
 46    )
 47    @ddt.unpack
 48    def test_concurrent_requests(self, transaction_decorator_name, exception_class, created_in_1, created_in_2):
 49        """
 50        Test that when isolation level is set to READ COMMITTED get_or_create()
 51        for the same row in concurrent requests does not raise an IntegrityError.
 52        """
 53        transaction_decorator = self.DECORATORS[transaction_decorator_name]
 54        if connection.vendor != 'mysql':
 55            raise unittest.SkipTest('Only works on MySQL.')
 56
 57        class RequestThread(threading.Thread):
 58            """ A thread which runs a dummy view."""
 59            def __init__(self, delay, **kwargs):
 60                super(RequestThread, self).__init__(**kwargs)
 61                self.delay = delay
 62                self.status = {}
 63
 64            @transaction_decorator
 65            def run(self):
 66                """A dummy view."""
 67                try:
 68                    try:
 69                        User.objects.get(username='student', email='student@edx.org')
 70                    except User.DoesNotExist:
 71                        pass
 72                    else:
 73                        raise AssertionError('Did not raise User.DoesNotExist.')
 74
 75                    if self.delay > 0:
 76                        time.sleep(self.delay)
 77
 78                    __, created = User.objects.get_or_create(username='student', email='student@edx.org')
 79                except Exception as exception:  # pylint: disable=broad-except
 80                    self.status['exception'] = exception
 81                else:
 82                    self.status['created'] = created
 83
 84        thread1 = RequestThread(delay=1)
 85        thread2 = RequestThread(delay=0)
 86
 87        thread1.start()
 88        thread2.start()
 89        thread2.join()
 90        thread1.join()
 91
 92        self.assertIsInstance(thread1.status.get('exception'), exception_class)
 93        self.assertEqual(thread1.status.get('created'), created_in_1)
 94
 95        self.assertIsNone(thread2.status.get('exception'))
 96        self.assertEqual(thread2.status.get('created'), created_in_2)
 97
 98    def test_outer_atomic_nesting(self):
 99        """
100        Test that outer_atomic raises an error if it is nested inside
101        another atomic.
102        """
103        if connection.vendor != 'mysql':
104            raise unittest.SkipTest('Only works on MySQL.')
105
106        outer_atomic()(do_nothing)()
107
108        with atomic():
109            atomic()(do_nothing)()
110
111        with outer_atomic():
112            atomic()(do_nothing)()
113
114        with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'):
115            with atomic():
116                outer_atomic()(do_nothing)()
117
118        with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'):
119            with outer_atomic():
120                outer_atomic()(do_nothing)()
121
122    def test_commit_on_success_nesting(self):
123        """
124        Test that commit_on_success raises an error if it is nested inside
125        atomic or if the isolation level is changed when it is nested
126        inside another commit_on_success.
127        """
128        # pylint: disable=not-callable
129
130        if connection.vendor != 'mysql':
131            raise unittest.SkipTest('Only works on MySQL.')
132
133        commit_on_success(read_committed=True)(do_nothing)()
134
135        with self.assertRaisesRegexp(TransactionManagementError, 'Cannot change isolation level when nested.'):
136            with commit_on_success():
137                commit_on_success(read_committed=True)(do_nothing)()
138
139        with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'):
140            with atomic():
141                commit_on_success(read_committed=True)(do_nothing)()
142
143    def test_named_outer_atomic_nesting(self):
144        """
145        Test that a named outer_atomic raises an error only if nested in
146        enable_named_outer_atomic and inside another atomic.
147        """
148        if connection.vendor != 'mysql':
149            raise unittest.SkipTest('Only works on MySQL.')
150
151        outer_atomic(name='abc')(do_nothing)()
152
153        with atomic():
154            outer_atomic(name='abc')(do_nothing)()
155
156        with enable_named_outer_atomic('abc'):
157
158            outer_atomic(name='abc')(do_nothing)()  # Not nested.
159
160            with atomic():
161                outer_atomic(name='pqr')(do_nothing)()  # Not enabled.
162
163            with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'):
164                with atomic():
165                    outer_atomic(name='abc')(do_nothing)()
166
167        with enable_named_outer_atomic('abc', 'def'):
168
169            outer_atomic(name='def')(do_nothing)()  # Not nested.
170
171            with atomic():
172                outer_atomic(name='pqr')(do_nothing)()  # Not enabled.
173
174            with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'):
175                with atomic():
176                    outer_atomic(name='def')(do_nothing)()
177
178            with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'):
179                with outer_atomic():
180                    outer_atomic(name='def')(do_nothing)()
181
182            with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'):
183                with atomic():
184                    outer_atomic(name='abc')(do_nothing)()
185
186            with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'):
187                with outer_atomic():
188                    outer_atomic(name='abc')(do_nothing)()
189
190
191@ddt.ddt
192class GenerateIntIdTestCase(TestCase):
193    """Tests for `generate_int_id`"""
194    @ddt.data(10)
195    def test_no_used_ids(self, times):
196        """
197        Verify that we get a random integer within the specified range
198        when there are no used ids.
199        """
200        minimum = 1
201        maximum = times
202        for __ in range(times):
203            self.assertIn(generate_int_id(minimum, maximum), range(minimum, maximum + 1))
204
205    @ddt.data(10)
206    def test_used_ids(self, times):
207        """
208        Verify that we get a random integer within the specified range
209        but not in a list of used ids.
210        """
211        minimum = 1
212        maximum = times
213        used_ids = {2, 4, 6, 8}
214        for __ in range(times):
215            int_id = generate_int_id(minimum, maximum, used_ids)
216            self.assertIn(int_id, list(set(range(minimum, maximum + 1)) - used_ids))
217
218
219class MigrationTests(TestCase):
220    """
221    Tests for migrations.
222    """
223    @override_settings(MIGRATION_MODULES={})
224    def test_migrations_are_in_sync(self):
225        """
226        Tests that the migration files are in sync with the models.
227        If this fails, you needs to run the Django command makemigrations.
228
229        The test is set up to override MIGRATION_MODULES to ensure migrations are
230        enabled for purposes of this test regardless of the overall test settings.
231
232        TODO: Find a general way of handling the case where if we're trying to
233        make a migrationless release that'll require a separate migration
234        release afterwards, this test doesn't fail.
235        """
236        out = StringIO()
237        call_command('makemigrations', dry_run=True, verbosity=3, stdout=out)
238        output = out.getvalue()
239        self.assertIn('No changes detected', output)