diff options
| author | Aymeric Augustin <aymeric.augustin@m4x.org> | 2013-03-11 15:11:34 +0100 |
|---|---|---|
| committer | Aymeric Augustin <aymeric.augustin@m4x.org> | 2013-03-11 15:11:34 +0100 |
| commit | 14cddf51c5f001bb426ce7f7a83fdc52c8d8aee9 (patch) | |
| tree | 7658f47dfaafe0dd09fe62fd5525e8f6e6a41ba7 /tests | |
| parent | 9cec689e6a7e299b3416519ee075b2316ecc5a64 (diff) | |
| parent | e654180ce2a11ef4c525497d6c40dc542e16806c (diff) | |
Merged branch 'database-level-autocommit'.
Fixed #2227: `atomic` supports nesting.
Fixed #6623: `commit_manually` is deprecated and `atomic` doesn't suffer from this defect.
Fixed #8320: the problem wasn't identified, but the legacy transaction management is deprecated.
Fixed #10744: the problem wasn't identified, but the legacy transaction management is deprecated.
Fixed #10813: since autocommit is enabled, it isn't necessary to rollback after errors any more.
Fixed #13742: savepoints are now implemented for SQLite.
Fixed #13870: transaction management in long running processes isn't a problem any more, and it's documented.
Fixed #14970: while it digresses on transaction management, this ticket essentially asks for autocommit on PostgreSQL.
Fixed #15694: `atomic` supports nesting.
Fixed #17887: autocommit makes it impossible for a connection to stay "idle of transaction".
Diffstat (limited to 'tests')
| -rw-r--r-- | tests/backends/tests.py | 19 | ||||
| -rw-r--r-- | tests/delete_regress/tests.py | 4 | ||||
| -rw-r--r-- | tests/fixtures_model_package/tests.py | 11 | ||||
| -rw-r--r-- | tests/fixtures_regress/tests.py | 7 | ||||
| -rw-r--r-- | tests/handlers/tests.py | 30 | ||||
| -rw-r--r-- | tests/handlers/urls.py | 9 | ||||
| -rw-r--r-- | tests/handlers/views.py | 17 | ||||
| -rw-r--r-- | tests/middleware/tests.py | 22 | ||||
| -rw-r--r-- | tests/requests/tests.py | 2 | ||||
| -rw-r--r-- | tests/select_for_update/tests.py | 3 | ||||
| -rw-r--r-- | tests/serializers/tests.py | 1 | ||||
| -rw-r--r-- | tests/transactions/models.py | 2 | ||||
| -rw-r--r-- | tests/transactions/tests.py | 314 | ||||
| -rw-r--r-- | tests/transactions_regress/tests.py | 95 |
14 files changed, 435 insertions, 101 deletions
diff --git a/tests/backends/tests.py b/tests/backends/tests.py index 103a44684e..c7f09013d4 100644 --- a/tests/backends/tests.py +++ b/tests/backends/tests.py @@ -302,8 +302,8 @@ class PostgresNewConnectionTest(TestCase): transaction is rolled back. """ @unittest.skipUnless( - connection.vendor == 'postgresql' and connection.isolation_level > 0, - "This test applies only to PostgreSQL without autocommit") + connection.vendor == 'postgresql', + "This test applies only to PostgreSQL") def test_connect_and_rollback(self): new_connections = ConnectionHandler(settings.DATABASES) new_connection = new_connections[DEFAULT_DB_ALIAS] @@ -522,7 +522,8 @@ class FkConstraintsTests(TransactionTestCase): """ When constraint checks are disabled, should be able to write bad data without IntegrityErrors. """ - with transaction.commit_manually(): + transaction.set_autocommit(False) + try: # Create an Article. models.Article.objects.create(headline="Test article", pub_date=datetime.datetime(2010, 9, 4), reporter=self.r) # Retrive it from the DB @@ -536,12 +537,15 @@ class FkConstraintsTests(TransactionTestCase): self.fail("IntegrityError should not have occurred.") finally: transaction.rollback() + finally: + transaction.set_autocommit(True) def test_disable_constraint_checks_context_manager(self): """ When constraint checks are disabled (using context manager), should be able to write bad data without IntegrityErrors. """ - with transaction.commit_manually(): + transaction.set_autocommit(False) + try: # Create an Article. models.Article.objects.create(headline="Test article", pub_date=datetime.datetime(2010, 9, 4), reporter=self.r) # Retrive it from the DB @@ -554,12 +558,15 @@ class FkConstraintsTests(TransactionTestCase): self.fail("IntegrityError should not have occurred.") finally: transaction.rollback() + finally: + transaction.set_autocommit(True) def test_check_constraints(self): """ Constraint checks should raise an IntegrityError when bad data is in the DB. """ - with transaction.commit_manually(): + try: + transaction.set_autocommit(False) # Create an Article. models.Article.objects.create(headline="Test article", pub_date=datetime.datetime(2010, 9, 4), reporter=self.r) # Retrive it from the DB @@ -572,6 +579,8 @@ class FkConstraintsTests(TransactionTestCase): connection.check_constraints() finally: transaction.rollback() + finally: + transaction.set_autocommit(True) class ThreadTests(TestCase): diff --git a/tests/delete_regress/tests.py b/tests/delete_regress/tests.py index 9fcc19ba71..e88c95e229 100644 --- a/tests/delete_regress/tests.py +++ b/tests/delete_regress/tests.py @@ -22,9 +22,7 @@ class DeleteLockingTest(TransactionTestCase): self.conn2 = new_connections[DEFAULT_DB_ALIAS] # Put both DB connections into managed transaction mode transaction.enter_transaction_management() - transaction.managed(True) self.conn2.enter_transaction_management() - self.conn2.managed(True) def tearDown(self): # Close down the second connection. @@ -335,7 +333,7 @@ class Ticket19102Tests(TestCase): ).select_related('orgunit').delete() self.assertFalse(Login.objects.filter(pk=self.l1.pk).exists()) self.assertTrue(Login.objects.filter(pk=self.l2.pk).exists()) - + @skipUnlessDBFeature("update_can_self_select") def test_ticket_19102_defer(self): with self.assertNumQueries(1): diff --git a/tests/fixtures_model_package/tests.py b/tests/fixtures_model_package/tests.py index d147fe68a7..c250f647ce 100644 --- a/tests/fixtures_model_package/tests.py +++ b/tests/fixtures_model_package/tests.py @@ -25,7 +25,8 @@ class SampleTestCase(TestCase): class TestNoInitialDataLoading(TransactionTestCase): def test_syncdb(self): - with transaction.commit_manually(): + transaction.set_autocommit(False) + try: Book.objects.all().delete() management.call_command( @@ -35,6 +36,9 @@ class TestNoInitialDataLoading(TransactionTestCase): ) self.assertQuerysetEqual(Book.objects.all(), []) transaction.rollback() + finally: + transaction.set_autocommit(True) + def test_flush(self): # Test presence of fixture (flush called by TransactionTestCase) @@ -45,7 +49,8 @@ class TestNoInitialDataLoading(TransactionTestCase): lambda a: a.name ) - with transaction.commit_manually(): + transaction.set_autocommit(False) + try: management.call_command( 'flush', verbosity=0, @@ -55,6 +60,8 @@ class TestNoInitialDataLoading(TransactionTestCase): ) self.assertQuerysetEqual(Book.objects.all(), []) transaction.rollback() + finally: + transaction.set_autocommit(True) class FixtureTestCase(TestCase): diff --git a/tests/fixtures_regress/tests.py b/tests/fixtures_regress/tests.py index 61dc4460df..c76056b93a 100644 --- a/tests/fixtures_regress/tests.py +++ b/tests/fixtures_regress/tests.py @@ -684,5 +684,8 @@ class TestTicket11101(TransactionTestCase): @skipUnlessDBFeature('supports_transactions') def test_ticket_11101(self): """Test that fixtures can be rolled back (ticket #11101).""" - ticket_11101 = transaction.commit_manually(self.ticket_11101) - ticket_11101() + transaction.set_autocommit(False) + try: + self.ticket_11101() + finally: + transaction.set_autocommit(True) diff --git a/tests/handlers/tests.py b/tests/handlers/tests.py index 6eb9bd23fe..3680eecdd2 100644 --- a/tests/handlers/tests.py +++ b/tests/handlers/tests.py @@ -1,9 +1,8 @@ from django.core.handlers.wsgi import WSGIHandler from django.core.signals import request_started, request_finished -from django.db import close_old_connections -from django.test import RequestFactory, TestCase +from django.db import close_old_connections, connection +from django.test import RequestFactory, TestCase, TransactionTestCase from django.test.utils import override_settings -from django.utils import six class HandlerTests(TestCase): @@ -37,6 +36,31 @@ class HandlerTests(TestCase): self.assertEqual(response.status_code, 400) +class TransactionsPerRequestTests(TransactionTestCase): + urls = 'handlers.urls' + + def test_no_transaction(self): + response = self.client.get('/in_transaction/') + self.assertContains(response, 'False') + + def test_auto_transaction(self): + old_atomic_requests = connection.settings_dict['ATOMIC_REQUESTS'] + try: + connection.settings_dict['ATOMIC_REQUESTS'] = True + response = self.client.get('/in_transaction/') + finally: + connection.settings_dict['ATOMIC_REQUESTS'] = old_atomic_requests + self.assertContains(response, 'True') + + def test_no_auto_transaction(self): + old_atomic_requests = connection.settings_dict['ATOMIC_REQUESTS'] + try: + connection.settings_dict['ATOMIC_REQUESTS'] = True + response = self.client.get('/not_in_transaction/') + finally: + connection.settings_dict['ATOMIC_REQUESTS'] = old_atomic_requests + self.assertContains(response, 'False') + class SignalsTests(TestCase): urls = 'handlers.urls' diff --git a/tests/handlers/urls.py b/tests/handlers/urls.py index 8570f04696..29858055ab 100644 --- a/tests/handlers/urls.py +++ b/tests/handlers/urls.py @@ -1,9 +1,12 @@ from __future__ import unicode_literals from django.conf.urls import patterns, url -from django.http import HttpResponse, StreamingHttpResponse + +from . import views urlpatterns = patterns('', - url(r'^regular/$', lambda request: HttpResponse(b"regular content")), - url(r'^streaming/$', lambda request: StreamingHttpResponse([b"streaming", b" ", b"content"])), + url(r'^regular/$', views.regular), + url(r'^streaming/$', views.streaming), + url(r'^in_transaction/$', views.in_transaction), + url(r'^not_in_transaction/$', views.not_in_transaction), ) diff --git a/tests/handlers/views.py b/tests/handlers/views.py new file mode 100644 index 0000000000..22d9ea4c7d --- /dev/null +++ b/tests/handlers/views.py @@ -0,0 +1,17 @@ +from __future__ import unicode_literals + +from django.db import connection +from django.http import HttpResponse, StreamingHttpResponse + +def regular(request): + return HttpResponse(b"regular content") + +def streaming(request): + return StreamingHttpResponse([b"streaming", b" ", b"content"]) + +def in_transaction(request): + return HttpResponse(str(connection.in_atomic_block)) + +def not_in_transaction(request): + return HttpResponse(str(connection.in_atomic_block)) +not_in_transaction.transactions_per_request = False diff --git a/tests/middleware/tests.py b/tests/middleware/tests.py index 4e5fd8ea6b..74b8ab9601 100644 --- a/tests/middleware/tests.py +++ b/tests/middleware/tests.py @@ -22,6 +22,9 @@ from django.test.utils import override_settings from django.utils import six from django.utils.encoding import force_str from django.utils.six.moves import xrange +from django.utils.unittest import expectedFailure + +from transactions.tests import IgnorePendingDeprecationWarningsMixin from .models import Band @@ -669,11 +672,12 @@ class ETagGZipMiddlewareTest(TestCase): self.assertNotEqual(gzip_etag, nogzip_etag) -class TransactionMiddlewareTest(TransactionTestCase): +class TransactionMiddlewareTest(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): """ Test the transaction middleware. """ def setUp(self): + super(TransactionMiddlewareTest, self).setUp() self.request = HttpRequest() self.request.META = { 'SERVER_NAME': 'testserver', @@ -685,33 +689,22 @@ class TransactionMiddlewareTest(TransactionTestCase): def tearDown(self): transaction.abort() + super(TransactionMiddlewareTest, self).tearDown() def test_request(self): TransactionMiddleware().process_request(self.request) - self.assertTrue(transaction.is_managed()) + self.assertFalse(transaction.get_autocommit()) def test_managed_response(self): transaction.enter_transaction_management() - transaction.managed(True) Band.objects.create(name='The Beatles') self.assertTrue(transaction.is_dirty()) TransactionMiddleware().process_response(self.request, self.response) self.assertFalse(transaction.is_dirty()) self.assertEqual(Band.objects.count(), 1) - def test_unmanaged_response(self): - transaction.enter_transaction_management() - transaction.managed(False) - self.assertEqual(Band.objects.count(), 0) - TransactionMiddleware().process_response(self.request, self.response) - self.assertFalse(transaction.is_managed()) - # The transaction middleware doesn't commit/rollback if management - # has been disabled. - self.assertTrue(transaction.is_dirty()) - def test_exception(self): transaction.enter_transaction_management() - transaction.managed(True) Band.objects.create(name='The Beatles') self.assertTrue(transaction.is_dirty()) TransactionMiddleware().process_exception(self.request, None) @@ -726,7 +719,6 @@ class TransactionMiddlewareTest(TransactionTestCase): raise IntegrityError() connections[DEFAULT_DB_ALIAS].commit = raise_exception transaction.enter_transaction_management() - transaction.managed(True) Band.objects.create(name='The Beatles') self.assertTrue(transaction.is_dirty()) with self.assertRaises(IntegrityError): diff --git a/tests/requests/tests.py b/tests/requests/tests.py index 4fdc17618b..2803d7995b 100644 --- a/tests/requests/tests.py +++ b/tests/requests/tests.py @@ -576,7 +576,6 @@ class DatabaseConnectionHandlingTests(TransactionTestCase): # Make sure there is an open connection connection.cursor() connection.enter_transaction_management() - connection.managed(True) signals.request_finished.send(sender=response._handler_class) self.assertEqual(len(connection.transaction_state), 0) @@ -585,7 +584,6 @@ class DatabaseConnectionHandlingTests(TransactionTestCase): connection.settings_dict['CONN_MAX_AGE'] = 0 connection.enter_transaction_management() - connection.managed(True) connection.set_dirty() # Test that the rollback doesn't succeed (for example network failure # could cause this). diff --git a/tests/select_for_update/tests.py b/tests/select_for_update/tests.py index b9716bd797..c2fa22705a 100644 --- a/tests/select_for_update/tests.py +++ b/tests/select_for_update/tests.py @@ -25,7 +25,6 @@ class SelectForUpdateTests(TransactionTestCase): def setUp(self): transaction.enter_transaction_management() - transaction.managed(True) self.person = Person.objects.create(name='Reinhardt') # We have to commit here so that code in run_select_for_update can @@ -37,7 +36,6 @@ class SelectForUpdateTests(TransactionTestCase): new_connections = ConnectionHandler(settings.DATABASES) self.new_connection = new_connections[DEFAULT_DB_ALIAS] self.new_connection.enter_transaction_management() - self.new_connection.managed(True) # We need to set settings.DEBUG to True so we can capture # the output SQL to examine. @@ -162,7 +160,6 @@ class SelectForUpdateTests(TransactionTestCase): # We need to enter transaction management again, as this is done on # per-thread basis transaction.enter_transaction_management() - transaction.managed(True) people = list( Person.objects.all().select_for_update(nowait=nowait) ) diff --git a/tests/serializers/tests.py b/tests/serializers/tests.py index 34d0f5f1b1..a96a1af748 100644 --- a/tests/serializers/tests.py +++ b/tests/serializers/tests.py @@ -268,7 +268,6 @@ class SerializersTransactionTestBase(object): # within a transaction in order to test forward reference # handling. transaction.enter_transaction_management() - transaction.managed(True) objs = serializers.deserialize(self.serializer_name, self.fwd_ref_str) with connection.constraint_checks_disabled(): for obj in objs: diff --git a/tests/transactions/models.py b/tests/transactions/models.py index 0f8d6b16ec..6c2bcfd23f 100644 --- a/tests/transactions/models.py +++ b/tests/transactions/models.py @@ -22,4 +22,4 @@ class Reporter(models.Model): ordering = ('first_name', 'last_name') def __str__(self): - return "%s %s" % (self.first_name, self.last_name) + return ("%s %s" % (self.first_name, self.last_name)).strip() diff --git a/tests/transactions/tests.py b/tests/transactions/tests.py index a1edf53fcb..e5a608e583 100644 --- a/tests/transactions/tests.py +++ b/tests/transactions/tests.py @@ -1,12 +1,320 @@ from __future__ import absolute_import +import sys +import warnings + from django.db import connection, transaction, IntegrityError from django.test import TransactionTestCase, skipUnlessDBFeature +from django.utils import six +from django.utils.unittest import skipUnless from .models import Reporter -class TransactionTests(TransactionTestCase): +@skipUnless(connection.features.uses_savepoints, + "'atomic' requires transactions and savepoints.") +class AtomicTests(TransactionTestCase): + """ + Tests for the atomic decorator and context manager. + + The tests make assertions on internal attributes because there isn't a + robust way to ask the database for its current transaction state. + + Since the decorator syntax is converted into a context manager (see the + implementation), there are only a few basic tests with the decorator + syntax and the bulk of the tests use the context manager syntax. + """ + + def test_decorator_syntax_commit(self): + @transaction.atomic + def make_reporter(): + Reporter.objects.create(first_name="Tintin") + make_reporter() + self.assertQuerysetEqual(Reporter.objects.all(), ['<Reporter: Tintin>']) + + def test_decorator_syntax_rollback(self): + @transaction.atomic + def make_reporter(): + Reporter.objects.create(first_name="Haddock") + raise Exception("Oops, that's his last name") + with six.assertRaisesRegex(self, Exception, "Oops"): + make_reporter() + self.assertQuerysetEqual(Reporter.objects.all(), []) + + def test_alternate_decorator_syntax_commit(self): + @transaction.atomic() + def make_reporter(): + Reporter.objects.create(first_name="Tintin") + make_reporter() + self.assertQuerysetEqual(Reporter.objects.all(), ['<Reporter: Tintin>']) + + def test_alternate_decorator_syntax_rollback(self): + @transaction.atomic() + def make_reporter(): + Reporter.objects.create(first_name="Haddock") + raise Exception("Oops, that's his last name") + with six.assertRaisesRegex(self, Exception, "Oops"): + make_reporter() + self.assertQuerysetEqual(Reporter.objects.all(), []) + + def test_commit(self): + with transaction.atomic(): + Reporter.objects.create(first_name="Tintin") + self.assertQuerysetEqual(Reporter.objects.all(), ['<Reporter: Tintin>']) + + def test_rollback(self): + with six.assertRaisesRegex(self, Exception, "Oops"): + with transaction.atomic(): + Reporter.objects.create(first_name="Haddock") + raise Exception("Oops, that's his last name") + self.assertQuerysetEqual(Reporter.objects.all(), []) + + def test_nested_commit_commit(self): + with transaction.atomic(): + Reporter.objects.create(first_name="Tintin") + with transaction.atomic(): + Reporter.objects.create(first_name="Archibald", last_name="Haddock") + self.assertQuerysetEqual(Reporter.objects.all(), + ['<Reporter: Archibald Haddock>', '<Reporter: Tintin>']) + + def test_nested_commit_rollback(self): + with transaction.atomic(): + Reporter.objects.create(first_name="Tintin") + with six.assertRaisesRegex(self, Exception, "Oops"): + with transaction.atomic(): + Reporter.objects.create(first_name="Haddock") + raise Exception("Oops, that's his last name") + self.assertQuerysetEqual(Reporter.objects.all(), ['<Reporter: Tintin>']) + + def test_nested_rollback_commit(self): + with six.assertRaisesRegex(self, Exception, "Oops"): + with transaction.atomic(): + Reporter.objects.create(last_name="Tintin") + with transaction.atomic(): + Reporter.objects.create(last_name="Haddock") + raise Exception("Oops, that's his first name") + self.assertQuerysetEqual(Reporter.objects.all(), []) + + def test_nested_rollback_rollback(self): + with six.assertRaisesRegex(self, Exception, "Oops"): + with transaction.atomic(): + Reporter.objects.create(last_name="Tintin") + with six.assertRaisesRegex(self, Exception, "Oops"): + with transaction.atomic(): + Reporter.objects.create(first_name="Haddock") + raise Exception("Oops, that's his last name") + raise Exception("Oops, that's his first name") + self.assertQuerysetEqual(Reporter.objects.all(), []) + + def test_merged_commit_commit(self): + with transaction.atomic(): + Reporter.objects.create(first_name="Tintin") + with transaction.atomic(savepoint=False): + Reporter.objects.create(first_name="Archibald", last_name="Haddock") + self.assertQuerysetEqual(Reporter.objects.all(), + ['<Reporter: Archibald Haddock>', '<Reporter: Tintin>']) + + def test_merged_commit_rollback(self): + with transaction.atomic(): + Reporter.objects.create(first_name="Tintin") + with six.assertRaisesRegex(self, Exception, "Oops"): + with transaction.atomic(savepoint=False): + Reporter.objects.create(first_name="Haddock") + raise Exception("Oops, that's his last name") + # Writes in the outer block are rolled back too. + self.assertQuerysetEqual(Reporter.objects.all(), []) + + def test_merged_rollback_commit(self): + with six.assertRaisesRegex(self, Exception, "Oops"): + with transaction.atomic(): + Reporter.objects.create(last_name="Tintin") + with transaction.atomic(savepoint=False): + Reporter.objects.create(last_name="Haddock") + raise Exception("Oops, that's his first name") + self.assertQuerysetEqual(Reporter.objects.all(), []) + + def test_merged_rollback_rollback(self): + with six.assertRaisesRegex(self, Exception, "Oops"): + with transaction.atomic(): + Reporter.objects.create(last_name="Tintin") + with six.assertRaisesRegex(self, Exception, "Oops"): + with transaction.atomic(savepoint=False): + Reporter.objects.create(first_name="Haddock") + raise Exception("Oops, that's his last name") + raise Exception("Oops, that's his first name") + self.assertQuerysetEqual(Reporter.objects.all(), []) + + def test_reuse_commit_commit(self): + atomic = transaction.atomic() + with atomic: + Reporter.objects.create(first_name="Tintin") + with atomic: + Reporter.objects.create(first_name="Archibald", last_name="Haddock") + self.assertQuerysetEqual(Reporter.objects.all(), + ['<Reporter: Archibald Haddock>', '<Reporter: Tintin>']) + + def test_reuse_commit_rollback(self): + atomic = transaction.atomic() + with atomic: + Reporter.objects.create(first_name="Tintin") + with six.assertRaisesRegex(self, Exception, "Oops"): + with atomic: + Reporter.objects.create(first_name="Haddock") + raise Exception("Oops, that's his last name") + self.assertQuerysetEqual(Reporter.objects.all(), ['<Reporter: Tintin>']) + + def test_reuse_rollback_commit(self): + atomic = transaction.atomic() + with six.assertRaisesRegex(self, Exception, "Oops"): + with atomic: + Reporter.objects.create(last_name="Tintin") + with atomic: + Reporter.objects.create(last_name="Haddock") + raise Exception("Oops, that's his first name") + self.assertQuerysetEqual(Reporter.objects.all(), []) + + def test_reuse_rollback_rollback(self): + atomic = transaction.atomic() + with six.assertRaisesRegex(self, Exception, "Oops"): + with atomic: + Reporter.objects.create(last_name="Tintin") + with six.assertRaisesRegex(self, Exception, "Oops"): + with atomic: + Reporter.objects.create(first_name="Haddock") + raise Exception("Oops, that's his last name") + raise Exception("Oops, that's his first name") + self.assertQuerysetEqual(Reporter.objects.all(), []) + + +class AtomicInsideTransactionTests(AtomicTests): + """All basic tests for atomic should also pass within an existing transaction.""" + + def setUp(self): + self.atomic = transaction.atomic() + self.atomic.__enter__() + + def tearDown(self): + self.atomic.__exit__(*sys.exc_info()) + + +class AtomicInsideLegacyTransactionManagementTests(AtomicTests): + + def setUp(self): + transaction.enter_transaction_management() + + def tearDown(self): + # The tests access the database after exercising 'atomic', making the + # connection dirty; a rollback is required to make it clean. + transaction.rollback() + transaction.leave_transaction_management() + + +@skipUnless(connection.features.uses_savepoints, + "'atomic' requires transactions and savepoints.") +class AtomicMergeTests(TransactionTestCase): + """Test merging transactions with savepoint=False.""" + + def test_merged_outer_rollback(self): + with transaction.atomic(): + Reporter.objects.create(first_name="Tintin") + with transaction.atomic(savepoint=False): + Reporter.objects.create(first_name="Archibald", last_name="Haddock") + with six.assertRaisesRegex(self, Exception, "Oops"): + with transaction.atomic(savepoint=False): + Reporter.objects.create(first_name="Tournesol") + raise Exception("Oops, that's his last name") + # It wasn't possible to roll back + self.assertEqual(Reporter.objects.count(), 3) + # It wasn't possible to roll back + self.assertEqual(Reporter.objects.count(), 3) + # The outer block must roll back + self.assertQuerysetEqual(Reporter.objects.all(), []) + + def test_merged_inner_savepoint_rollback(self): + with transaction.atomic(): + Reporter.objects.create(first_name="Tintin") + with transaction.atomic(): + Reporter.objects.create(first_name="Archibald", last_name="Haddock") + with six.assertRaisesRegex(self, Exception, "Oops"): + with transaction.atomic(savepoint=False): + Reporter.objects.create(first_name="Tournesol") + raise Exception("Oops, that's his last name") + # It wasn't possible to roll back + self.assertEqual(Reporter.objects.count(), 3) + # The first block with a savepoint must roll back + self.assertEqual(Reporter.objects.count(), 1) + self.assertQuerysetEqual(Reporter.objects.all(), ['<Reporter: Tintin>']) + + def test_merged_outer_rollback_after_inner_failure_and_inner_success(self): + with transaction.atomic(): + Reporter.objects.create(first_name="Tintin") + # Inner block without a savepoint fails + with six.assertRaisesRegex(self, Exception, "Oops"): + with transaction.atomic(savepoint=False): + Reporter.objects.create(first_name="Haddock") + raise Exception("Oops, that's his last name") + # It wasn't possible to roll back + self.assertEqual(Reporter.objects.count(), 2) + # Inner block with a savepoint succeeds + with transaction.atomic(savepoint=False): + Reporter.objects.create(first_name="Archibald", last_name="Haddock") + # It still wasn't possible to roll back + self.assertEqual(Reporter.objects.count(), 3) + # The outer block must rollback + self.assertQuerysetEqual(Reporter.objects.all(), []) + + +@skipUnless(connection.features.uses_savepoints, + "'atomic' requires transactions and savepoints.") +class AtomicErrorsTests(TransactionTestCase): + + def test_atomic_requires_autocommit(self): + transaction.set_autocommit(False) + try: + with self.assertRaises(transaction.TransactionManagementError): + with transaction.atomic(): + pass + finally: + transaction.set_autocommit(True) + + def test_atomic_prevents_disabling_autocommit(self): + autocommit = transaction.get_autocommit() + with transaction.atomic(): + with self.assertRaises(transaction.TransactionManagementError): + transaction.set_autocommit(not autocommit) + # Make sure autocommit wasn't changed. + self.assertEqual(connection.autocommit, autocommit) + + def test_atomic_prevents_calling_transaction_methods(self): + with transaction.atomic(): + with self.assertRaises(transaction.TransactionManagementError): + transaction.commit() + with self.assertRaises(transaction.TransactionManagementError): + transaction.rollback() + + def test_atomic_prevents_calling_transaction_management_methods(self): + with transaction.atomic(): + with self.assertRaises(transaction.TransactionManagementError): + transaction.enter_transaction_management() + with self.assertRaises(transaction.TransactionManagementError): + transaction.leave_transaction_management() + + +class IgnorePendingDeprecationWarningsMixin(object): + + def setUp(self): + super(IgnorePendingDeprecationWarningsMixin, self).setUp() + self.catch_warnings = warnings.catch_warnings() + self.catch_warnings.__enter__() + warnings.filterwarnings("ignore", category=PendingDeprecationWarning) + + def tearDown(self): + self.catch_warnings.__exit__(*sys.exc_info()) + super(IgnorePendingDeprecationWarningsMixin, self).tearDown() + + +class TransactionTests(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): + def create_a_reporter_then_fail(self, first, last): a = Reporter(first_name=first, last_name=last) a.save() @@ -161,7 +469,7 @@ class TransactionTests(TransactionTestCase): ) -class TransactionRollbackTests(TransactionTestCase): +class TransactionRollbackTests(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): def execute_bad_sql(self): cursor = connection.cursor() cursor.execute("INSERT INTO transactions_reporter (first_name, last_name) VALUES ('Douglas', 'Adams');") @@ -178,7 +486,7 @@ class TransactionRollbackTests(TransactionTestCase): self.assertRaises(IntegrityError, execute_bad_sql) transaction.rollback() -class TransactionContextManagerTests(TransactionTestCase): +class TransactionContextManagerTests(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): def create_reporter_and_fail(self): Reporter.objects.create(first_name="Bob", last_name="Holtzman") raise Exception diff --git a/tests/transactions_regress/tests.py b/tests/transactions_regress/tests.py index 6ba04892cd..142c09d3cf 100644 --- a/tests/transactions_regress/tests.py +++ b/tests/transactions_regress/tests.py @@ -6,10 +6,12 @@ from django.test import TransactionTestCase, skipUnlessDBFeature from django.test.utils import override_settings from django.utils.unittest import skipIf, skipUnless +from transactions.tests import IgnorePendingDeprecationWarningsMixin + from .models import Mod, M2mA, M2mB -class TestTransactionClosing(TransactionTestCase): +class TestTransactionClosing(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): """ Tests to make sure that transactions are properly closed when they should be, and aren't left pending after operations @@ -166,17 +168,13 @@ class TestTransactionClosing(TransactionTestCase): (connection.settings_dict['NAME'] == ':memory:' or not connection.settings_dict['NAME']), 'Test uses multiple connections, but in-memory sqlite does not support this') -class TestNewConnection(TransactionTestCase): +class TestNewConnection(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): """ Check that new connections don't have special behaviour. """ def setUp(self): self._old_backend = connections[DEFAULT_DB_ALIAS] settings = self._old_backend.settings_dict.copy() - opts = settings['OPTIONS'].copy() - if 'autocommit' in opts: - opts['autocommit'] = False - settings['OPTIONS'] = opts new_backend = self._old_backend.__class__(settings, DEFAULT_DB_ALIAS) connections[DEFAULT_DB_ALIAS] = new_backend @@ -193,16 +191,20 @@ class TestNewConnection(TransactionTestCase): """ Users are allowed to commit and rollback connections. """ - # The starting value is False, not None. - self.assertIs(connection._dirty, False) - list(Mod.objects.all()) - self.assertTrue(connection.is_dirty()) - connection.commit() - self.assertFalse(connection.is_dirty()) - list(Mod.objects.all()) - self.assertTrue(connection.is_dirty()) - connection.rollback() - self.assertFalse(connection.is_dirty()) + connection.set_autocommit(False) + try: + # The starting value is False, not None. + self.assertIs(connection._dirty, False) + list(Mod.objects.all()) + self.assertTrue(connection.is_dirty()) + connection.commit() + self.assertFalse(connection.is_dirty()) + list(Mod.objects.all()) + self.assertTrue(connection.is_dirty()) + connection.rollback() + self.assertFalse(connection.is_dirty()) + finally: + connection.set_autocommit(True) def test_enter_exit_management(self): orig_dirty = connection._dirty @@ -210,39 +212,10 @@ class TestNewConnection(TransactionTestCase): connection.leave_transaction_management() self.assertEqual(orig_dirty, connection._dirty) - def test_commit_unless_managed(self): - cursor = connection.cursor() - cursor.execute("INSERT into transactions_regress_mod (fld) values (2)") - connection.commit_unless_managed() - self.assertFalse(connection.is_dirty()) - self.assertEqual(len(Mod.objects.all()), 1) - self.assertTrue(connection.is_dirty()) - connection.commit_unless_managed() - self.assertFalse(connection.is_dirty()) - - def test_commit_unless_managed_in_managed(self): - cursor = connection.cursor() - connection.enter_transaction_management() - transaction.managed(True) - cursor.execute("INSERT into transactions_regress_mod (fld) values (2)") - connection.commit_unless_managed() - self.assertTrue(connection.is_dirty()) - connection.rollback() - self.assertFalse(connection.is_dirty()) - self.assertEqual(len(Mod.objects.all()), 0) - connection.commit() - connection.leave_transaction_management() - self.assertFalse(connection.is_dirty()) - self.assertEqual(len(Mod.objects.all()), 0) - self.assertTrue(connection.is_dirty()) - connection.commit_unless_managed() - self.assertFalse(connection.is_dirty()) - self.assertEqual(len(Mod.objects.all()), 0) - @skipUnless(connection.vendor == 'postgresql', "This test only valid for PostgreSQL") -class TestPostgresAutocommitAndIsolation(TransactionTestCase): +class TestPostgresAutocommitAndIsolation(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): """ Tests to make sure psycopg2's autocommit mode and isolation level is restored after entering and leaving transaction management. @@ -261,7 +234,6 @@ class TestPostgresAutocommitAndIsolation(TransactionTestCase): self._old_backend = connections[DEFAULT_DB_ALIAS] settings = self._old_backend.settings_dict.copy() opts = settings['OPTIONS'].copy() - opts['autocommit'] = True opts['isolation_level'] = ISOLATION_LEVEL_SERIALIZABLE settings['OPTIONS'] = opts new_backend = self._old_backend.__class__(settings, DEFAULT_DB_ALIAS) @@ -275,40 +247,42 @@ class TestPostgresAutocommitAndIsolation(TransactionTestCase): connections[DEFAULT_DB_ALIAS] = self._old_backend def test_initial_autocommit_state(self): - self.assertTrue(connection.features.uses_autocommit) - self.assertEqual(connection.isolation_level, self._autocommit) + # Autocommit is activated when the connection is created. + connection.cursor().close() + self.assertTrue(connection.autocommit) def test_transaction_management(self): transaction.enter_transaction_management() - transaction.managed(True) + self.assertFalse(connection.autocommit) self.assertEqual(connection.isolation_level, self._serializable) transaction.leave_transaction_management() - self.assertEqual(connection.isolation_level, self._autocommit) + self.assertTrue(connection.autocommit) def test_transaction_stacking(self): transaction.enter_transaction_management() - transaction.managed(True) + self.assertFalse(connection.autocommit) self.assertEqual(connection.isolation_level, self._serializable) transaction.enter_transaction_management() + self.assertFalse(connection.autocommit) self.assertEqual(connection.isolation_level, self._serializable) transaction.leave_transaction_management() + self.assertFalse(connection.autocommit) self.assertEqual(connection.isolation_level, self._serializable) transaction.leave_transaction_management() - self.assertEqual(connection.isolation_level, self._autocommit) + self.assertTrue(connection.autocommit) def test_enter_autocommit(self): transaction.enter_transaction_management() - transaction.managed(True) + self.assertFalse(connection.autocommit) self.assertEqual(connection.isolation_level, self._serializable) list(Mod.objects.all()) self.assertTrue(transaction.is_dirty()) # Enter autocommit mode again. transaction.enter_transaction_management(False) - transaction.managed(False) self.assertFalse(transaction.is_dirty()) self.assertEqual( connection.connection.get_transaction_status(), @@ -316,12 +290,13 @@ class TestPostgresAutocommitAndIsolation(TransactionTestCase): list(Mod.objects.all()) self.assertFalse(transaction.is_dirty()) transaction.leave_transaction_management() + self.assertFalse(connection.autocommit) self.assertEqual(connection.isolation_level, self._serializable) transaction.leave_transaction_management() - self.assertEqual(connection.isolation_level, self._autocommit) + self.assertTrue(connection.autocommit) -class TestManyToManyAddTransaction(TransactionTestCase): +class TestManyToManyAddTransaction(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): def test_manyrelated_add_commit(self): "Test for https://code.djangoproject.com/ticket/16818" a = M2mA.objects.create() @@ -336,8 +311,10 @@ class TestManyToManyAddTransaction(TransactionTestCase): self.assertEqual(a.others.count(), 1) -class SavepointTest(TransactionTestCase): +class SavepointTest(IgnorePendingDeprecationWarningsMixin, TransactionTestCase): + @skipIf(connection.vendor == 'sqlite', + "SQLite doesn't support savepoints in managed mode") @skipUnlessDBFeature('uses_savepoints') def test_savepoint_commit(self): @commit_manually @@ -353,6 +330,8 @@ class SavepointTest(TransactionTestCase): work() + @skipIf(connection.vendor == 'sqlite', + "SQLite doesn't support savepoints in managed mode") @skipIf(connection.vendor == 'mysql' and connection.features._mysql_storage_engine == 'MyISAM', "MyISAM MySQL storage engine doesn't support savepoints") |
