summaryrefslogtreecommitdiff
path: root/tests/transaction_hooks
diff options
context:
space:
mode:
authorAndreas Pelme <andreas@pelme.se>2015-06-30 18:18:56 +0200
committerTim Graham <timograham@gmail.com>2015-06-30 14:51:00 -0400
commit00a1d4d042a7afd139316982c9b57e87d26a894f (patch)
tree65b4427112045acc25d097413b34faeab882ad88 /tests/transaction_hooks
parent9f0d67137c98aa296471e1b7f57ae43f5bb17db6 (diff)
downloaddjango-00a1d4d042a7afd139316982c9b57e87d26a894f.tar.gz
Fixed #21803 -- Added support for post-commit callbacks
Made it possible to register and run callbacks after a database transaction is committed with the `transaction.on_commit()` function. This patch is heavily based on Carl Meyers django-transaction-hooks <https://django-transaction-hooks.readthedocs.org/>. Thanks to Aymeric Augustin, Carl Meyer, and Tim Graham for review and feedback.
Diffstat (limited to 'tests/transaction_hooks')
-rw-r--r--tests/transaction_hooks/__init__.py0
-rw-r--r--tests/transaction_hooks/models.py10
-rw-r--r--tests/transaction_hooks/tests.py220
3 files changed, 230 insertions, 0 deletions
diff --git a/tests/transaction_hooks/__init__.py b/tests/transaction_hooks/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/tests/transaction_hooks/__init__.py
diff --git a/tests/transaction_hooks/models.py b/tests/transaction_hooks/models.py
new file mode 100644
index 0000000000..cd2f22b514
--- /dev/null
+++ b/tests/transaction_hooks/models.py
@@ -0,0 +1,10 @@
+from django.db import models
+from django.utils.encoding import python_2_unicode_compatible
+
+
+@python_2_unicode_compatible
+class Thing(models.Model):
+ num = models.IntegerField()
+
+ def __str__(self):
+ return "Thing %d" % self.num
diff --git a/tests/transaction_hooks/tests.py b/tests/transaction_hooks/tests.py
new file mode 100644
index 0000000000..ebf07bc656
--- /dev/null
+++ b/tests/transaction_hooks/tests.py
@@ -0,0 +1,220 @@
+from django.db import connection, transaction
+from django.test import TransactionTestCase, skipUnlessDBFeature
+
+from .models import Thing
+
+
+class ForcedError(Exception):
+ pass
+
+
+class TestConnectionOnCommit(TransactionTestCase):
+ """
+ Tests for transaction.on_commit().
+
+ Creation/checking of database objects in parallel with callback tracking is
+ to verify that the behavior of the two match in all tested cases.
+ """
+ available_apps = ['transaction_hooks']
+
+ def setUp(self):
+ self.notified = []
+
+ def notify(self, id_):
+ if id_ == 'error':
+ raise ForcedError()
+ self.notified.append(id_)
+
+ def do(self, num):
+ """Create a Thing instance and notify about it."""
+ Thing.objects.create(num=num)
+ transaction.on_commit(lambda: self.notify(num))
+
+ def assertDone(self, nums):
+ self.assertNotified(nums)
+ self.assertEqual(sorted(t.num for t in Thing.objects.all()), sorted(nums))
+
+ def assertNotified(self, nums):
+ self.assertEqual(self.notified, nums)
+
+ def test_executes_immediately_if_no_transaction(self):
+ self.do(1)
+ self.assertDone([1])
+
+ def test_delays_execution_until_after_transaction_commit(self):
+ with transaction.atomic():
+ self.do(1)
+ self.assertNotified([])
+ self.assertDone([1])
+
+ def test_does_not_execute_if_transaction_rolled_back(self):
+ try:
+ with transaction.atomic():
+ self.do(1)
+ raise ForcedError()
+ except ForcedError:
+ pass
+
+ self.assertDone([])
+
+ def test_executes_only_after_final_transaction_committed(self):
+ with transaction.atomic():
+ with transaction.atomic():
+ self.do(1)
+ self.assertNotified([])
+ self.assertNotified([])
+ self.assertDone([1])
+
+ def test_discards_hooks_from_rolled_back_savepoint(self):
+ with transaction.atomic():
+ # one successful savepoint
+ with transaction.atomic():
+ self.do(1)
+ # one failed savepoint
+ try:
+ with transaction.atomic():
+ self.do(2)
+ raise ForcedError()
+ except ForcedError:
+ pass
+ # another successful savepoint
+ with transaction.atomic():
+ self.do(3)
+
+ # only hooks registered during successful savepoints execute
+ self.assertDone([1, 3])
+
+ def test_no_hooks_run_from_failed_transaction(self):
+ """If outer transaction fails, no hooks from within it run."""
+ try:
+ with transaction.atomic():
+ with transaction.atomic():
+ self.do(1)
+ raise ForcedError()
+ except ForcedError:
+ pass
+
+ self.assertDone([])
+
+ def test_inner_savepoint_rolled_back_with_outer(self):
+ with transaction.atomic():
+ try:
+ with transaction.atomic():
+ with transaction.atomic():
+ self.do(1)
+ raise ForcedError()
+ except ForcedError:
+ pass
+ self.do(2)
+
+ self.assertDone([2])
+
+ def test_no_savepoints_atomic_merged_with_outer(self):
+ with transaction.atomic():
+ with transaction.atomic():
+ self.do(1)
+ try:
+ with transaction.atomic(savepoint=False):
+ raise ForcedError()
+ except ForcedError:
+ pass
+
+ self.assertDone([])
+
+ def test_inner_savepoint_does_not_affect_outer(self):
+ with transaction.atomic():
+ with transaction.atomic():
+ self.do(1)
+ try:
+ with transaction.atomic():
+ raise ForcedError()
+ except ForcedError:
+ pass
+
+ self.assertDone([1])
+
+ def test_runs_hooks_in_order_registered(self):
+ with transaction.atomic():
+ self.do(1)
+ with transaction.atomic():
+ self.do(2)
+ self.do(3)
+
+ self.assertDone([1, 2, 3])
+
+ def test_hooks_cleared_after_successful_commit(self):
+ with transaction.atomic():
+ self.do(1)
+ with transaction.atomic():
+ self.do(2)
+
+ self.assertDone([1, 2]) # not [1, 1, 2]
+
+ def test_hooks_cleared_after_rollback(self):
+ try:
+ with transaction.atomic():
+ self.do(1)
+ raise ForcedError()
+ except ForcedError:
+ pass
+
+ with transaction.atomic():
+ self.do(2)
+
+ self.assertDone([2])
+
+ @skipUnlessDBFeature('test_db_allows_multiple_connections')
+ def test_hooks_cleared_on_reconnect(self):
+ with transaction.atomic():
+ self.do(1)
+ connection.close()
+
+ connection.connect()
+
+ with transaction.atomic():
+ self.do(2)
+
+ self.assertDone([2])
+
+ def test_error_in_hook_doesnt_prevent_clearing_hooks(self):
+ try:
+ with transaction.atomic():
+ transaction.on_commit(lambda: self.notify('error'))
+ except ForcedError:
+ pass
+
+ with transaction.atomic():
+ self.do(1)
+
+ self.assertDone([1])
+
+ def test_db_query_in_hook(self):
+ with transaction.atomic():
+ Thing.objects.create(num=1)
+ transaction.on_commit(
+ lambda: [self.notify(t.num) for t in Thing.objects.all()]
+ )
+
+ self.assertDone([1])
+
+ def test_transaction_in_hook(self):
+ def on_commit():
+ with transaction.atomic():
+ t = Thing.objects.create(num=1)
+ self.notify(t.num)
+
+ with transaction.atomic():
+ transaction.on_commit(on_commit)
+
+ self.assertDone([1])
+
+ def test_raises_exception_non_autocommit_mode(self):
+ def should_never_be_called():
+ raise AssertionError('this function should never be called')
+
+ try:
+ connection.set_autocommit(False)
+ with self.assertRaises(transaction.TransactionManagementError):
+ transaction.on_commit(should_never_be_called)
+ finally:
+ connection.set_autocommit(True)