tests.py 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  1. from django.db import connection, transaction
  2. from django.test import TransactionTestCase, skipUnlessDBFeature
  3. from .models import Thing
  4. class ForcedError(Exception):
  5. pass
  6. @skipUnlessDBFeature("supports_transactions")
  7. class TestConnectionOnCommit(TransactionTestCase):
  8. """
  9. Tests for transaction.on_commit().
  10. Creation/checking of database objects in parallel with callback tracking is
  11. to verify that the behavior of the two match in all tested cases.
  12. """
  13. available_apps = ["transaction_hooks"]
  14. def setUp(self):
  15. self.notified = []
  16. def notify(self, id_):
  17. if id_ == "error":
  18. raise ForcedError()
  19. self.notified.append(id_)
  20. def do(self, num):
  21. """Create a Thing instance and notify about it."""
  22. Thing.objects.create(num=num)
  23. transaction.on_commit(lambda: self.notify(num))
  24. def assertDone(self, nums):
  25. self.assertNotified(nums)
  26. self.assertEqual(sorted(t.num for t in Thing.objects.all()), sorted(nums))
  27. def assertNotified(self, nums):
  28. self.assertEqual(self.notified, nums)
  29. def test_executes_immediately_if_no_transaction(self):
  30. self.do(1)
  31. self.assertDone([1])
  32. def test_robust_if_no_transaction(self):
  33. def robust_callback():
  34. raise ForcedError("robust callback")
  35. with self.assertLogs("django.db.backends.base", "ERROR") as cm:
  36. transaction.on_commit(robust_callback, robust=True)
  37. self.do(1)
  38. self.assertDone([1])
  39. log_record = cm.records[0]
  40. self.assertEqual(
  41. log_record.getMessage(),
  42. "Error calling TestConnectionOnCommit.test_robust_if_no_transaction."
  43. "<locals>.robust_callback in on_commit() (robust callback).",
  44. )
  45. self.assertIsNotNone(log_record.exc_info)
  46. raised_exception = log_record.exc_info[1]
  47. self.assertIsInstance(raised_exception, ForcedError)
  48. self.assertEqual(str(raised_exception), "robust callback")
  49. def test_robust_transaction(self):
  50. def robust_callback():
  51. raise ForcedError("robust callback")
  52. with self.assertLogs("django.db.backends", "ERROR") as cm:
  53. with transaction.atomic():
  54. transaction.on_commit(robust_callback, robust=True)
  55. self.do(1)
  56. self.assertDone([1])
  57. log_record = cm.records[0]
  58. self.assertEqual(
  59. log_record.getMessage(),
  60. "Error calling TestConnectionOnCommit.test_robust_transaction.<locals>."
  61. "robust_callback in on_commit() during transaction (robust callback).",
  62. )
  63. self.assertIsNotNone(log_record.exc_info)
  64. raised_exception = log_record.exc_info[1]
  65. self.assertIsInstance(raised_exception, ForcedError)
  66. self.assertEqual(str(raised_exception), "robust callback")
  67. def test_delays_execution_until_after_transaction_commit(self):
  68. with transaction.atomic():
  69. self.do(1)
  70. self.assertNotified([])
  71. self.assertDone([1])
  72. def test_does_not_execute_if_transaction_rolled_back(self):
  73. try:
  74. with transaction.atomic():
  75. self.do(1)
  76. raise ForcedError()
  77. except ForcedError:
  78. pass
  79. self.assertDone([])
  80. def test_executes_only_after_final_transaction_committed(self):
  81. with transaction.atomic():
  82. with transaction.atomic():
  83. self.do(1)
  84. self.assertNotified([])
  85. self.assertNotified([])
  86. self.assertDone([1])
  87. def test_discards_hooks_from_rolled_back_savepoint(self):
  88. with transaction.atomic():
  89. # one successful savepoint
  90. with transaction.atomic():
  91. self.do(1)
  92. # one failed savepoint
  93. try:
  94. with transaction.atomic():
  95. self.do(2)
  96. raise ForcedError()
  97. except ForcedError:
  98. pass
  99. # another successful savepoint
  100. with transaction.atomic():
  101. self.do(3)
  102. # only hooks registered during successful savepoints execute
  103. self.assertDone([1, 3])
  104. def test_no_hooks_run_from_failed_transaction(self):
  105. """If outer transaction fails, no hooks from within it run."""
  106. try:
  107. with transaction.atomic():
  108. with transaction.atomic():
  109. self.do(1)
  110. raise ForcedError()
  111. except ForcedError:
  112. pass
  113. self.assertDone([])
  114. def test_inner_savepoint_rolled_back_with_outer(self):
  115. with transaction.atomic():
  116. try:
  117. with transaction.atomic():
  118. with transaction.atomic():
  119. self.do(1)
  120. raise ForcedError()
  121. except ForcedError:
  122. pass
  123. self.do(2)
  124. self.assertDone([2])
  125. def test_no_savepoints_atomic_merged_with_outer(self):
  126. with transaction.atomic():
  127. with transaction.atomic():
  128. self.do(1)
  129. try:
  130. with transaction.atomic(savepoint=False):
  131. raise ForcedError()
  132. except ForcedError:
  133. pass
  134. self.assertDone([])
  135. def test_inner_savepoint_does_not_affect_outer(self):
  136. with transaction.atomic():
  137. with transaction.atomic():
  138. self.do(1)
  139. try:
  140. with transaction.atomic():
  141. raise ForcedError()
  142. except ForcedError:
  143. pass
  144. self.assertDone([1])
  145. def test_runs_hooks_in_order_registered(self):
  146. with transaction.atomic():
  147. self.do(1)
  148. with transaction.atomic():
  149. self.do(2)
  150. self.do(3)
  151. self.assertDone([1, 2, 3])
  152. def test_hooks_cleared_after_successful_commit(self):
  153. with transaction.atomic():
  154. self.do(1)
  155. with transaction.atomic():
  156. self.do(2)
  157. self.assertDone([1, 2]) # not [1, 1, 2]
  158. def test_hooks_cleared_after_rollback(self):
  159. try:
  160. with transaction.atomic():
  161. self.do(1)
  162. raise ForcedError()
  163. except ForcedError:
  164. pass
  165. with transaction.atomic():
  166. self.do(2)
  167. self.assertDone([2])
  168. @skipUnlessDBFeature("test_db_allows_multiple_connections")
  169. def test_hooks_cleared_on_reconnect(self):
  170. with transaction.atomic():
  171. self.do(1)
  172. connection.close()
  173. connection.connect()
  174. with transaction.atomic():
  175. self.do(2)
  176. self.assertDone([2])
  177. def test_error_in_hook_doesnt_prevent_clearing_hooks(self):
  178. try:
  179. with transaction.atomic():
  180. transaction.on_commit(lambda: self.notify("error"))
  181. except ForcedError:
  182. pass
  183. with transaction.atomic():
  184. self.do(1)
  185. self.assertDone([1])
  186. def test_db_query_in_hook(self):
  187. with transaction.atomic():
  188. Thing.objects.create(num=1)
  189. transaction.on_commit(
  190. lambda: [self.notify(t.num) for t in Thing.objects.all()]
  191. )
  192. self.assertDone([1])
  193. def test_transaction_in_hook(self):
  194. def on_commit():
  195. with transaction.atomic():
  196. t = Thing.objects.create(num=1)
  197. self.notify(t.num)
  198. with transaction.atomic():
  199. transaction.on_commit(on_commit)
  200. self.assertDone([1])
  201. def test_hook_in_hook(self):
  202. def on_commit(i, add_hook):
  203. with transaction.atomic():
  204. if add_hook:
  205. transaction.on_commit(lambda: on_commit(i + 10, False))
  206. t = Thing.objects.create(num=i)
  207. self.notify(t.num)
  208. with transaction.atomic():
  209. transaction.on_commit(lambda: on_commit(1, True))
  210. transaction.on_commit(lambda: on_commit(2, True))
  211. self.assertDone([1, 11, 2, 12])
  212. def test_raises_exception_non_autocommit_mode(self):
  213. def should_never_be_called():
  214. raise AssertionError("this function should never be called")
  215. try:
  216. connection.set_autocommit(False)
  217. msg = "on_commit() cannot be used in manual transaction management"
  218. with self.assertRaisesMessage(transaction.TransactionManagementError, msg):
  219. transaction.on_commit(should_never_be_called)
  220. finally:
  221. connection.set_autocommit(True)
  222. def test_raises_exception_non_callable(self):
  223. msg = "on_commit()'s callback must be a callable."
  224. with self.assertRaisesMessage(TypeError, msg):
  225. transaction.on_commit(None)