A named outer is suitable when dealing with IntegrityErrors. It only checks that it is not nested under an atomic only if it is nested under enable_outer_atomic(name=<name>). This way only the view which is causing IntegrityErrors needs to have its automatic transaction management disabled and other callers are not effected.
226 lines
7.8 KiB
Python
226 lines
7.8 KiB
Python
"""Tests for util.db module."""
|
|
|
|
import ddt
|
|
import threading
|
|
import time
|
|
import unittest
|
|
from unittest import skipIf
|
|
|
|
from django.contrib.auth.models import User
|
|
from django.core.management import call_command
|
|
from django.conf import settings
|
|
from django.db import connection, IntegrityError
|
|
from django.db.transaction import atomic, TransactionManagementError
|
|
from django.test import TestCase, TransactionTestCase
|
|
|
|
from util.db import (
|
|
commit_on_success, enable_named_outer_atomic, outer_atomic, generate_int_id, NoOpMigrationModules
|
|
)
|
|
|
|
|
|
def do_nothing():
|
|
"""Just return."""
|
|
return
|
|
|
|
|
|
@ddt.ddt
|
|
class TransactionManagersTestCase(TransactionTestCase):
|
|
"""
|
|
Tests commit_on_success and outer_atomic.
|
|
|
|
Note: This TestCase only works with MySQL.
|
|
|
|
To test do: "./manage.py lms --settings=test_with_mysql test util.tests.test_db"
|
|
"""
|
|
|
|
@ddt.data(
|
|
(outer_atomic(), IntegrityError, None, True),
|
|
(outer_atomic(read_committed=True), type(None), False, True),
|
|
(commit_on_success(), IntegrityError, None, True),
|
|
(commit_on_success(read_committed=True), type(None), False, True),
|
|
)
|
|
@ddt.unpack
|
|
def test_concurrent_requests(self, transaction_decorator, exception_class, created_in_1, created_in_2):
|
|
"""
|
|
Test that when isolation level is set to READ COMMITTED get_or_create()
|
|
for the same row in concurrent requests does not raise an IntegrityError.
|
|
"""
|
|
|
|
if connection.vendor != 'mysql':
|
|
raise unittest.SkipTest('Only works on MySQL.')
|
|
|
|
class RequestThread(threading.Thread):
|
|
""" A thread which runs a dummy view."""
|
|
def __init__(self, delay, **kwargs):
|
|
super(RequestThread, self).__init__(**kwargs)
|
|
self.delay = delay
|
|
self.status = {}
|
|
|
|
@transaction_decorator
|
|
def run(self):
|
|
"""A dummy view."""
|
|
try:
|
|
try:
|
|
User.objects.get(username='student', email='student@edx.org')
|
|
except User.DoesNotExist:
|
|
pass
|
|
else:
|
|
raise AssertionError('Did not raise User.DoesNotExist.')
|
|
|
|
if self.delay > 0:
|
|
time.sleep(self.delay)
|
|
|
|
__, created = User.objects.get_or_create(username='student', email='student@edx.org')
|
|
except Exception as exception: # pylint: disable=broad-except
|
|
self.status['exception'] = exception
|
|
else:
|
|
self.status['created'] = created
|
|
|
|
thread1 = RequestThread(delay=1)
|
|
thread2 = RequestThread(delay=0)
|
|
|
|
thread1.start()
|
|
thread2.start()
|
|
thread2.join()
|
|
thread1.join()
|
|
|
|
self.assertIsInstance(thread1.status.get('exception'), exception_class)
|
|
self.assertEqual(thread1.status.get('created'), created_in_1)
|
|
|
|
self.assertIsNone(thread2.status.get('exception'))
|
|
self.assertEqual(thread2.status.get('created'), created_in_2)
|
|
|
|
def test_outer_atomic_nesting(self):
|
|
"""
|
|
Test that outer_atomic raises an error if it is nested inside
|
|
another atomic.
|
|
"""
|
|
if connection.vendor != 'mysql':
|
|
raise unittest.SkipTest('Only works on MySQL.')
|
|
|
|
outer_atomic()(do_nothing)()
|
|
|
|
with atomic():
|
|
atomic()(do_nothing)()
|
|
|
|
with outer_atomic():
|
|
atomic()(do_nothing)()
|
|
|
|
with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'):
|
|
with atomic():
|
|
outer_atomic()(do_nothing)()
|
|
|
|
with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'):
|
|
with outer_atomic():
|
|
outer_atomic()(do_nothing)()
|
|
|
|
def test_commit_on_success_nesting(self):
|
|
"""
|
|
Test that commit_on_success raises an error if it is nested inside
|
|
atomic or if the isolation level is changed when it is nested
|
|
inside another commit_on_success.
|
|
"""
|
|
# pylint: disable=not-callable
|
|
|
|
if connection.vendor != 'mysql':
|
|
raise unittest.SkipTest('Only works on MySQL.')
|
|
|
|
commit_on_success(read_committed=True)(do_nothing)()
|
|
|
|
with self.assertRaisesRegexp(TransactionManagementError, 'Cannot change isolation level when nested.'):
|
|
with commit_on_success():
|
|
commit_on_success(read_committed=True)(do_nothing)()
|
|
|
|
with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'):
|
|
with atomic():
|
|
commit_on_success(read_committed=True)(do_nothing)()
|
|
|
|
def test_named_outer_atomic_nesting(self):
|
|
"""
|
|
Test that a named outer_atomic raises an error only if nested in
|
|
enable_named_outer_atomic and inside another atomic.
|
|
"""
|
|
if connection.vendor != 'mysql':
|
|
raise unittest.SkipTest('Only works on MySQL.')
|
|
|
|
outer_atomic(name='abc')(do_nothing)()
|
|
|
|
with atomic():
|
|
outer_atomic(name='abc')(do_nothing)()
|
|
|
|
with enable_named_outer_atomic('abc'):
|
|
|
|
outer_atomic(name='abc')(do_nothing)() # Not nested.
|
|
|
|
with atomic():
|
|
outer_atomic(name='pqr')(do_nothing)() # Not enabled.
|
|
|
|
with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'):
|
|
with atomic():
|
|
outer_atomic(name='abc')(do_nothing)()
|
|
|
|
with enable_named_outer_atomic('abc', 'def'):
|
|
|
|
outer_atomic(name='def')(do_nothing)() # Not nested.
|
|
|
|
with atomic():
|
|
outer_atomic(name='pqr')(do_nothing)() # Not enabled.
|
|
|
|
with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'):
|
|
with atomic():
|
|
outer_atomic(name='def')(do_nothing)()
|
|
|
|
with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'):
|
|
with outer_atomic():
|
|
outer_atomic(name='def')(do_nothing)()
|
|
|
|
with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'):
|
|
with atomic():
|
|
outer_atomic(name='abc')(do_nothing)()
|
|
|
|
with self.assertRaisesRegexp(TransactionManagementError, 'Cannot be inside an atomic block.'):
|
|
with outer_atomic():
|
|
outer_atomic(name='abc')(do_nothing)()
|
|
|
|
|
|
@ddt.ddt
|
|
class GenerateIntIdTestCase(TestCase):
|
|
"""Tests for `generate_int_id`"""
|
|
@ddt.data(10)
|
|
def test_no_used_ids(self, times):
|
|
"""
|
|
Verify that we get a random integer within the specified range
|
|
when there are no used ids.
|
|
"""
|
|
minimum = 1
|
|
maximum = times
|
|
for i in range(times):
|
|
self.assertIn(generate_int_id(minimum, maximum), range(minimum, maximum + 1))
|
|
|
|
@ddt.data(10)
|
|
def test_used_ids(self, times):
|
|
"""
|
|
Verify that we get a random integer within the specified range
|
|
but not in a list of used ids.
|
|
"""
|
|
minimum = 1
|
|
maximum = times
|
|
used_ids = {2, 4, 6, 8}
|
|
for i in range(times):
|
|
int_id = generate_int_id(minimum, maximum, used_ids)
|
|
self.assertIn(int_id, list(set(range(minimum, maximum + 1)) - used_ids))
|
|
|
|
|
|
class MigrationTests(TestCase):
|
|
"""
|
|
Tests for migrations.
|
|
"""
|
|
@skipIf(isinstance(settings.MIGRATION_MODULES, NoOpMigrationModules), 'Skip in case of NoOpMigrationModules')
|
|
def test_migrations_are_in_sync(self):
|
|
"""
|
|
Tests that the migration files are in sync with the models.
|
|
If this fails, you needs to run the Django command makemigrations.
|
|
"""
|
|
with self.assertRaises(SystemExit):
|
|
call_command('makemigrations', '-e')
|