tests.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. from __future__ import unicode_literals
  2. from unittest import skipIf, skipUnless, SkipTest
  3. from django.db import (connection, connections, transaction, DEFAULT_DB_ALIAS, DatabaseError,
  4. IntegrityError)
  5. from django.db.transaction import commit_on_success, commit_manually, TransactionManagementError
  6. from django.test import TransactionTestCase, override_settings, skipUnlessDBFeature
  7. from django.test.utils import IgnoreDeprecationWarningsMixin
  8. from .models import Mod, M2mA, M2mB, SubMod
  9. class ModelInheritanceTests(TransactionTestCase):
  10. available_apps = ['transactions_regress']
  11. def test_save(self):
  12. # First, create a SubMod, then try to save another with conflicting
  13. # cnt field. The problem was that transactions were committed after
  14. # every parent save when not in managed transaction. As the cnt
  15. # conflict is in the second model, we can check if the first save
  16. # was committed or not.
  17. SubMod(fld=1, cnt=1).save()
  18. # We should have committed the transaction for the above - assert this.
  19. connection.rollback()
  20. self.assertEqual(SubMod.objects.count(), 1)
  21. try:
  22. SubMod(fld=2, cnt=1).save()
  23. except IntegrityError:
  24. connection.rollback()
  25. self.assertEqual(SubMod.objects.count(), 1)
  26. self.assertEqual(Mod.objects.count(), 1)
  27. class TestTransactionClosing(IgnoreDeprecationWarningsMixin, TransactionTestCase):
  28. """
  29. Tests to make sure that transactions are properly closed
  30. when they should be, and aren't left pending after operations
  31. have been performed in them. Refs #9964.
  32. """
  33. available_apps = [
  34. 'transactions_regress',
  35. 'django.contrib.auth',
  36. 'django.contrib.contenttypes',
  37. ]
  38. def test_raw_committed_on_success(self):
  39. """
  40. Make sure a transaction consisting of raw SQL execution gets
  41. committed by the commit_on_success decorator.
  42. """
  43. @commit_on_success
  44. def raw_sql():
  45. "Write a record using raw sql under a commit_on_success decorator"
  46. with connection.cursor() as cursor:
  47. cursor.execute("INSERT into transactions_regress_mod (fld) values (18)")
  48. raw_sql()
  49. # Rollback so that if the decorator didn't commit, the record is unwritten
  50. transaction.rollback()
  51. self.assertEqual(Mod.objects.count(), 1)
  52. # Check that the record is in the DB
  53. obj = Mod.objects.all()[0]
  54. self.assertEqual(obj.fld, 18)
  55. def test_commit_manually_enforced(self):
  56. """
  57. Make sure that under commit_manually, even "read-only" transaction require closure
  58. (commit or rollback), and a transaction left pending is treated as an error.
  59. """
  60. @commit_manually
  61. def non_comitter():
  62. "Execute a managed transaction with read-only operations and fail to commit"
  63. Mod.objects.count()
  64. self.assertRaises(TransactionManagementError, non_comitter)
  65. def test_commit_manually_commit_ok(self):
  66. """
  67. Test that under commit_manually, a committed transaction is accepted by the transaction
  68. management mechanisms
  69. """
  70. @commit_manually
  71. def committer():
  72. """
  73. Perform a database query, then commit the transaction
  74. """
  75. Mod.objects.count()
  76. transaction.commit()
  77. try:
  78. committer()
  79. except TransactionManagementError:
  80. self.fail("Commit did not clear the transaction state")
  81. def test_commit_manually_rollback_ok(self):
  82. """
  83. Test that under commit_manually, a rolled-back transaction is accepted by the transaction
  84. management mechanisms
  85. """
  86. @commit_manually
  87. def roller_back():
  88. """
  89. Perform a database query, then rollback the transaction
  90. """
  91. Mod.objects.count()
  92. transaction.rollback()
  93. try:
  94. roller_back()
  95. except TransactionManagementError:
  96. self.fail("Rollback did not clear the transaction state")
  97. def test_commit_manually_enforced_after_commit(self):
  98. """
  99. Test that under commit_manually, if a transaction is committed and an operation is
  100. performed later, we still require the new transaction to be closed
  101. """
  102. @commit_manually
  103. def fake_committer():
  104. "Query, commit, then query again, leaving with a pending transaction"
  105. Mod.objects.count()
  106. transaction.commit()
  107. Mod.objects.count()
  108. self.assertRaises(TransactionManagementError, fake_committer)
  109. @skipUnlessDBFeature('supports_transactions')
  110. def test_reuse_cursor_reference(self):
  111. """
  112. Make sure transaction closure is enforced even when the queries are performed
  113. through a single cursor reference retrieved in the beginning
  114. (this is to show why it is wrong to set the transaction dirty only when a cursor
  115. is fetched from the connection).
  116. """
  117. @commit_on_success
  118. def reuse_cursor_ref():
  119. """
  120. Fetch a cursor, perform an query, rollback to close the transaction,
  121. then write a record (in a new transaction) using the same cursor object
  122. (reference). All this under commit_on_success, so the second insert should
  123. be committed.
  124. """
  125. with connection.cursor() as cursor:
  126. cursor.execute("INSERT into transactions_regress_mod (fld) values (2)")
  127. transaction.rollback()
  128. cursor.execute("INSERT into transactions_regress_mod (fld) values (2)")
  129. reuse_cursor_ref()
  130. # Rollback so that if the decorator didn't commit, the record is unwritten
  131. transaction.rollback()
  132. self.assertEqual(Mod.objects.count(), 1)
  133. obj = Mod.objects.all()[0]
  134. self.assertEqual(obj.fld, 2)
  135. def test_failing_query_transaction_closed(self):
  136. """
  137. Make sure that under commit_on_success, a transaction is rolled back even if
  138. the first database-modifying operation fails.
  139. This is prompted by http://code.djangoproject.com/ticket/6669 (and based on sample
  140. code posted there to exemplify the problem): Before Django 1.3,
  141. transactions were only marked "dirty" by the save() function after it successfully
  142. wrote the object to the database.
  143. """
  144. from django.contrib.auth.models import User
  145. @transaction.commit_on_success
  146. def create_system_user():
  147. "Create a user in a transaction"
  148. user = User.objects.create_user(username='system', password='iamr00t',
  149. email='root@SITENAME.com')
  150. # Redundant, just makes sure the user id was read back from DB
  151. Mod.objects.create(fld=user.pk)
  152. # Create a user
  153. create_system_user()
  154. with self.assertRaises(DatabaseError):
  155. # The second call to create_system_user should fail for violating
  156. # a unique constraint (it's trying to re-create the same user)
  157. create_system_user()
  158. # Try to read the database. If the last transaction was indeed closed,
  159. # this should cause no problems
  160. User.objects.all()[0]
  161. @override_settings(DEBUG=True)
  162. def test_failing_query_transaction_closed_debug(self):
  163. """
  164. Regression for #6669. Same test as above, with DEBUG=True.
  165. """
  166. self.test_failing_query_transaction_closed()
  167. @skipIf(connection.vendor == 'sqlite'
  168. and connection.settings_dict['TEST_NAME'] in (None, '', ':memory:'),
  169. "Cannot establish two connections to an in-memory SQLite database.")
  170. class TestNewConnection(IgnoreDeprecationWarningsMixin, TransactionTestCase):
  171. """
  172. Check that new connections don't have special behaviour.
  173. """
  174. available_apps = ['transactions_regress']
  175. def setUp(self):
  176. self._old_backend = connections[DEFAULT_DB_ALIAS]
  177. settings = self._old_backend.settings_dict.copy()
  178. new_backend = self._old_backend.__class__(settings, DEFAULT_DB_ALIAS)
  179. connections[DEFAULT_DB_ALIAS] = new_backend
  180. def tearDown(self):
  181. try:
  182. connections[DEFAULT_DB_ALIAS].abort()
  183. connections[DEFAULT_DB_ALIAS].close()
  184. finally:
  185. connections[DEFAULT_DB_ALIAS] = self._old_backend
  186. def test_commit(self):
  187. """
  188. Users are allowed to commit and rollback connections.
  189. """
  190. connection.set_autocommit(False)
  191. try:
  192. # The starting value is False, not None.
  193. self.assertIs(connection._dirty, False)
  194. list(Mod.objects.all())
  195. self.assertTrue(connection.is_dirty())
  196. connection.commit()
  197. self.assertFalse(connection.is_dirty())
  198. list(Mod.objects.all())
  199. self.assertTrue(connection.is_dirty())
  200. connection.rollback()
  201. self.assertFalse(connection.is_dirty())
  202. finally:
  203. connection.set_autocommit(True)
  204. def test_enter_exit_management(self):
  205. orig_dirty = connection._dirty
  206. connection.enter_transaction_management()
  207. connection.leave_transaction_management()
  208. self.assertEqual(orig_dirty, connection._dirty)
  209. @skipUnless(connection.vendor == 'postgresql',
  210. "This test only valid for PostgreSQL")
  211. class TestPostgresAutocommitAndIsolation(IgnoreDeprecationWarningsMixin, TransactionTestCase):
  212. """
  213. Tests to make sure psycopg2's autocommit mode and isolation level
  214. is restored after entering and leaving transaction management.
  215. Refs #16047, #18130.
  216. """
  217. available_apps = ['transactions_regress']
  218. def setUp(self):
  219. from psycopg2.extensions import (ISOLATION_LEVEL_AUTOCOMMIT,
  220. ISOLATION_LEVEL_SERIALIZABLE,
  221. TRANSACTION_STATUS_IDLE)
  222. self._autocommit = ISOLATION_LEVEL_AUTOCOMMIT
  223. self._serializable = ISOLATION_LEVEL_SERIALIZABLE
  224. self._idle = TRANSACTION_STATUS_IDLE
  225. # We want a clean backend with autocommit = True, so
  226. # first we need to do a bit of work to have that.
  227. self._old_backend = connections[DEFAULT_DB_ALIAS]
  228. settings = self._old_backend.settings_dict.copy()
  229. opts = settings['OPTIONS'].copy()
  230. opts['isolation_level'] = ISOLATION_LEVEL_SERIALIZABLE
  231. settings['OPTIONS'] = opts
  232. new_backend = self._old_backend.__class__(settings, DEFAULT_DB_ALIAS)
  233. connections[DEFAULT_DB_ALIAS] = new_backend
  234. def tearDown(self):
  235. try:
  236. connections[DEFAULT_DB_ALIAS].abort()
  237. finally:
  238. connections[DEFAULT_DB_ALIAS].close()
  239. connections[DEFAULT_DB_ALIAS] = self._old_backend
  240. def test_initial_autocommit_state(self):
  241. # Autocommit is activated when the connection is created.
  242. connection.cursor().close()
  243. self.assertTrue(connection.autocommit)
  244. def test_transaction_management(self):
  245. transaction.enter_transaction_management()
  246. self.assertFalse(connection.autocommit)
  247. self.assertEqual(connection.isolation_level, self._serializable)
  248. transaction.leave_transaction_management()
  249. self.assertTrue(connection.autocommit)
  250. def test_transaction_stacking(self):
  251. transaction.enter_transaction_management()
  252. self.assertFalse(connection.autocommit)
  253. self.assertEqual(connection.isolation_level, self._serializable)
  254. transaction.enter_transaction_management()
  255. self.assertFalse(connection.autocommit)
  256. self.assertEqual(connection.isolation_level, self._serializable)
  257. transaction.leave_transaction_management()
  258. self.assertFalse(connection.autocommit)
  259. self.assertEqual(connection.isolation_level, self._serializable)
  260. transaction.leave_transaction_management()
  261. self.assertTrue(connection.autocommit)
  262. def test_enter_autocommit(self):
  263. transaction.enter_transaction_management()
  264. self.assertFalse(connection.autocommit)
  265. self.assertEqual(connection.isolation_level, self._serializable)
  266. list(Mod.objects.all())
  267. self.assertTrue(transaction.is_dirty())
  268. # Enter autocommit mode again.
  269. transaction.enter_transaction_management(False)
  270. self.assertFalse(transaction.is_dirty())
  271. self.assertEqual(
  272. connection.connection.get_transaction_status(),
  273. self._idle)
  274. list(Mod.objects.all())
  275. self.assertFalse(transaction.is_dirty())
  276. transaction.leave_transaction_management()
  277. self.assertFalse(connection.autocommit)
  278. self.assertEqual(connection.isolation_level, self._serializable)
  279. transaction.leave_transaction_management()
  280. self.assertTrue(connection.autocommit)
  281. class TestManyToManyAddTransaction(IgnoreDeprecationWarningsMixin, TransactionTestCase):
  282. available_apps = ['transactions_regress']
  283. def test_manyrelated_add_commit(self):
  284. "Test for https://code.djangoproject.com/ticket/16818"
  285. a = M2mA.objects.create()
  286. b = M2mB.objects.create(fld=10)
  287. a.others.add(b)
  288. # We're in a TransactionTestCase and have not changed transaction
  289. # behavior from default of "autocommit", so this rollback should not
  290. # actually do anything. If it does in fact undo our add, that's a bug
  291. # that the bulk insert was not auto-committed.
  292. transaction.rollback()
  293. self.assertEqual(a.others.count(), 1)
  294. class SavepointTest(IgnoreDeprecationWarningsMixin, TransactionTestCase):
  295. available_apps = ['transactions_regress']
  296. @skipIf(connection.vendor == 'sqlite',
  297. "SQLite doesn't support savepoints in managed mode")
  298. @skipUnlessDBFeature('uses_savepoints')
  299. def test_savepoint_commit(self):
  300. @commit_manually
  301. def work():
  302. mod = Mod.objects.create(fld=1)
  303. pk = mod.pk
  304. sid = transaction.savepoint()
  305. Mod.objects.filter(pk=pk).update(fld=10)
  306. transaction.savepoint_commit(sid)
  307. mod2 = Mod.objects.get(pk=pk)
  308. transaction.commit()
  309. self.assertEqual(mod2.fld, 10)
  310. work()
  311. @skipIf(connection.vendor == 'sqlite',
  312. "SQLite doesn't support savepoints in managed mode")
  313. @skipUnlessDBFeature('uses_savepoints')
  314. def test_savepoint_rollback(self):
  315. # _mysql_storage_engine issues a query and as such can't be applied in
  316. # a skipIf decorator since that would execute the query on module load.
  317. if (connection.vendor == 'mysql' and
  318. connection.features._mysql_storage_engine == 'MyISAM'):
  319. raise SkipTest("MyISAM MySQL storage engine doesn't support savepoints")
  320. @commit_manually
  321. def work():
  322. mod = Mod.objects.create(fld=1)
  323. pk = mod.pk
  324. sid = transaction.savepoint()
  325. Mod.objects.filter(pk=pk).update(fld=20)
  326. transaction.savepoint_rollback(sid)
  327. mod2 = Mod.objects.get(pk=pk)
  328. transaction.commit()
  329. self.assertEqual(mod2.fld, 1)
  330. work()