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