Code

Ticket #8138: django_transaction_tests_3.2.diff

File django_transaction_tests_3.2.diff, 14.9 KB (added by mremolt, 6 years ago)
Line 
1Index: django/test/client.py
2===================================================================
3--- django/test/client.py       (Revision 8302)
4+++ django/test/client.py       (Arbeitskopie)
5@@ -18,6 +18,7 @@
6 from django.utils.encoding import smart_str
7 from django.utils.http import urlencode
8 from django.utils.itercompat import is_iterable
9+from django.db import transaction
10 
11 BOUNDARY = 'BoUnDaRyStRiNg'
12 MULTIPART_CONTENT = 'multipart/form-data; boundary=%s' % BOUNDARY
13@@ -60,8 +61,12 @@
14 
15         signals.request_started.send(sender=self.__class__)
16         try:
17+            transaction.enter_transaction_management()
18+            transaction.managed(True)
19             request = WSGIRequest(environ)
20             response = self.get_response(request)
21+            transaction.commit()
22+            transaction.leave_transaction_management()
23 
24             # Apply response middleware.
25             for middleware_method in self._response_middleware:
26@@ -134,7 +139,7 @@
27         '',
28         file.read()
29     ]
30-   
31+
32 class Client:
33     """
34     A class that can act as a client for testing purposes.
35@@ -170,6 +175,10 @@
36         Obtains the current session variables.
37         """
38         if 'django.contrib.sessions' in settings.INSTALLED_APPS:
39+            # If a session db change hangs in a transaction, commit,
40+            # just to be sure.
41+            if transaction.is_dirty():
42+                transaction.commit()
43             engine = __import__(settings.SESSION_ENGINE, {}, {}, [''])
44             cookie = self.cookies.get(settings.SESSION_COOKIE_NAME, None)
45             if cookie:
46Index: django/test/testcases.py
47===================================================================
48--- django/test/testcases.py    (Revision 8302)
49+++ django/test/testcases.py    (Arbeitskopie)
50@@ -1,3 +1,4 @@
51+
52 import re
53 import unittest
54 from urlparse import urlsplit, urlunsplit
55@@ -8,6 +9,7 @@
56 from django.core.management import call_command
57 from django.core.urlresolvers import clear_url_caches
58 from django.db import transaction
59+from django.db.models.signals import post_save
60 from django.http import QueryDict
61 from django.test import _doctest as doctest
62 from django.test.client import Client
63@@ -55,7 +57,7 @@
64         """Tries to do a 'xml-comparision' of want and got.  Plain string
65         comparision doesn't always work because, for example, attribute
66         ordering should not be important.
67-       
68+
69         Based on http://codespeak.net/svn/lxml/trunk/src/lxml/doctestcompare.py
70         """
71         _norm_whitespace_re = re.compile(r'[ \t\n][ \t\n]+')
72@@ -102,7 +104,7 @@
73             wrapper = '<root>%s</root>'
74             want = wrapper % want
75             got = wrapper % got
76-           
77+
78         # Parse the want and got strings, and compare the parsings.
79         try:
80             want_root = parseString(want).firstChild
81@@ -169,27 +171,44 @@
82         # side effects on other tests.
83         transaction.rollback_unless_managed()
84 
85-class TestCase(unittest.TestCase):
86+    def run(self, test, compileflags=None, out=None, clear_globs=True):
87+        """
88+        Wraps the parent run() and encloses it in a transaction.
89+        """
90+        transaction.enter_transaction_management()
91+        transaction.managed(True)
92+        result = doctest.DocTestRunner.run(self, test, compileflags, out, clear_globs)
93+        transaction.rollback()
94+        transaction.leave_transaction_management()
95+        return result
96+
97+class TransactionTestCase(unittest.TestCase):
98     def _pre_setup(self):
99         """Performs any pre-test setup. This includes:
100 
101             * Flushing the database.
102-            * If the Test Case class has a 'fixtures' member, installing the
103+            * If the Test Case class has a 'fixtures' member, installing the
104               named fixtures.
105             * If the Test Case class has a 'urls' member, replace the
106               ROOT_URLCONF with it.
107             * Clearing the mail test outbox.
108         """
109+        self._fixture_setup()
110+        self._urlconf_setup()
111+        mail.outbox = []
112+
113+    def _fixture_setup(self):
114         call_command('flush', verbosity=0, interactive=False)
115         if hasattr(self, 'fixtures'):
116             # We have to use this slightly awkward syntax due to the fact
117             # that we're using *args and **kwargs together.
118             call_command('loaddata', *self.fixtures, **{'verbosity': 0})
119+
120+    def _urlconf_setup(self):
121         if hasattr(self, 'urls'):
122             self._old_root_urlconf = settings.ROOT_URLCONF
123             settings.ROOT_URLCONF = self.urls
124             clear_url_caches()
125-        mail.outbox = []
126 
127     def __call__(self, result=None):
128         """
129@@ -206,7 +225,7 @@
130             import sys
131             result.addError(self, sys.exc_info())
132             return
133-        super(TestCase, self).__call__(result)
134+        super(TransactionTestCase, self).__call__(result)
135         try:
136             self._post_teardown()
137         except (KeyboardInterrupt, SystemExit):
138@@ -221,6 +240,13 @@
139 
140             * Putting back the original ROOT_URLCONF if it was changed.
141         """
142+        self._fixture_teardown()
143+        self._urlconf_teardown()
144+
145+    def _fixture_teardown(self):
146+        pass
147+
148+    def _urlconf_teardown(self):
149         if hasattr(self, '_old_root_urlconf'):
150             settings.ROOT_URLCONF = self._old_root_urlconf
151             clear_url_caches()
152@@ -354,3 +380,49 @@
153         self.failIf(template_name in template_names,
154             (u"Template '%s' was used unexpectedly in rendering the"
155              u" response") % template_name)
156+
157+class TestCase(TransactionTestCase):
158+    """
159+    Does basically the same as TransactionTestCase, but surrounds every test
160+    with a transaction. You have to use TransactionTestCase, if you need
161+    transaction management inside a test.
162+    """
163+
164+    # has the db been changed during the test
165+    db_was_changed = False
166+
167+    def _fixture_setup(self):
168+        transaction.enter_transaction_management()
169+        transaction.managed(True)
170+
171+        # whenever a save occured, the db must be dirty
172+        post_save.connect(self._set_db_was_changed)
173+        # this seems more elegant than patching ClientHandler
174+        #request_started.connect(self._do_commit)
175+        #request_finished.connect(self._do_commit)
176+
177+        if hasattr(self, 'fixtures'):
178+            call_command('loaddata', *self.fixtures, **{
179+                                                        'verbosity': 0,
180+                                                        'no_commit': True
181+                                                        })
182+            # TODO: find out, if loaddata does emit a post_save signal
183+            self._set_db_was_changed()
184+
185+    def _fixture_teardown(self):
186+        # If the transaction is not dirty, but the DB was changed,
187+        # a commit must have happened, so flush instead of rollback.
188+        # This currently doesn't catch the following case:
189+        # Inside a test a commit happens and after that more data is changed.
190+        if not transaction.is_dirty() and self.db_was_changed:
191+            transaction.leave_transaction_management()
192+            call_command('flush', verbosity=0, interactive=False)
193+        else:
194+            transaction.rollback()
195+            transaction.leave_transaction_management()
196+
197+    def _set_db_was_changed(self, *args, **kwargs):
198+        self.db_was_changed = True
199+
200+    def _do_commit(self, *args, **kwargs):
201+        transaction.commit()
202Index: django/test/__init__.py
203===================================================================
204--- django/test/__init__.py     (Revision 8302)
205+++ django/test/__init__.py     (Arbeitskopie)
206@@ -3,4 +3,4 @@
207 """
208 
209 from django.test.client import Client
210-from django.test.testcases import TestCase
211+from django.test.testcases import TestCase, TransactionTestCase
212Index: django/core/management/commands/loaddata.py
213===================================================================
214--- django/core/management/commands/loaddata.py (Revision 8302)
215+++ django/core/management/commands/loaddata.py (Arbeitskopie)
216@@ -28,6 +28,7 @@
217 
218         verbosity = int(options.get('verbosity', 1))
219         show_traceback = options.get('traceback', False)
220+        no_commit = options.get('no_commit', False)
221 
222         # Keep a count of the installed objects and fixtures
223         fixture_count = 0
224@@ -41,12 +42,13 @@
225         # the side effect of initializing the test database (if
226         # it isn't already initialized).
227         cursor = connection.cursor()
228-
229+        print dir(connection)
230         # Start transaction management. All fixtures are installed in a
231         # single transaction to ensure that all references are resolved.
232-        transaction.commit_unless_managed()
233-        transaction.enter_transaction_management()
234-        transaction.managed(True)
235+        if not no_commit:
236+            transaction.commit_unless_managed()
237+            transaction.enter_transaction_management()
238+            transaction.managed(True)
239 
240         app_fixtures = [os.path.join(os.path.dirname(app.__file__), 'fixtures') for app in get_apps()]
241         for fixture_label in fixture_labels:
242@@ -133,7 +135,7 @@
243                                 (format, fixture_name, humanize(fixture_dir))
244 
245 
246-        # If any of the fixtures we loaded contain 0 objects, assume that an
247+        # If any of the fixtures we loaded contain 0 objects, assume that an
248         # error was encountered during fixture loading.
249         if 0 in objects_per_fixture:
250             sys.stderr.write(
251@@ -142,8 +144,8 @@
252             transaction.rollback()
253             transaction.leave_transaction_management()
254             return
255-           
256-        # If we found even one object in a fixture, we need to reset the
257+
258+        # If we found even one object in a fixture, we need to reset the
259         # database sequences.
260         if object_count > 0:
261             sequence_sql = connection.ops.sequence_reset_sql(self.style, models)
262@@ -152,19 +154,21 @@
263                     print "Resetting sequences"
264                 for line in sequence_sql:
265                     cursor.execute(line)
266-           
267-        transaction.commit()
268-        transaction.leave_transaction_management()
269 
270+        if not no_commit:
271+            transaction.commit()
272+            transaction.leave_transaction_management()
273+
274         if object_count == 0:
275             if verbosity > 1:
276                 print "No fixtures found."
277         else:
278             if verbosity > 0:
279                 print "Installed %d object(s) from %d fixture(s)" % (object_count, fixture_count)
280-               
281+
282         # Close the DB connection. This is required as a workaround for an
283         # edge case in MySQL: if the same connection is used to
284         # create tables, load data, and query, the query can return
285         # incorrect results. See Django #7572, MySQL #37735.
286-        connection.close()
287+        if not no_commit:
288+            connection.close()
289Index: tests/regressiontests/admin_views/tests.py
290===================================================================
291--- tests/regressiontests/admin_views/tests.py  (Revision 8302)
292+++ tests/regressiontests/admin_views/tests.py  (Arbeitskopie)
293@@ -1,6 +1,6 @@
294 # coding: utf-8
295 
296-from django.test import TestCase
297+from django.test import TestCase, TransactionTestCase
298 from django.contrib.auth.models import User, Permission
299 from django.contrib.contenttypes.models import ContentType
300 from django.contrib.admin.models import LogEntry
301@@ -13,13 +13,13 @@
302 
303 class AdminViewBasicTest(TestCase):
304     fixtures = ['admin-views-users.xml']
305-   
306+
307     def setUp(self):
308         self.client.login(username='super', password='secret')
309-   
310+
311     def tearDown(self):
312         self.client.logout()
313-   
314+
315     def testTrailingSlashRequired(self):
316         """
317         If you leave off the trailing slash, app should redirect and add it.
318@@ -28,21 +28,21 @@
319         self.assertRedirects(request,
320             '/test_admin/admin/admin_views/article/add/'
321         )
322-   
323+
324     def testBasicAddGet(self):
325         """
326         A smoke test to ensure GET on the add_view works.
327         """
328         response = self.client.get('/test_admin/admin/admin_views/section/add/')
329         self.failUnlessEqual(response.status_code, 200)
330-   
331+
332     def testBasicEditGet(self):
333         """
334         A smoke test to ensureGET on the change_view works.
335         """
336         response = self.client.get('/test_admin/admin/admin_views/section/1/')
337         self.failUnlessEqual(response.status_code, 200)
338-   
339+
340     def testBasicAddPost(self):
341         """
342         A smoke test to ensure POST on add_view works.
343@@ -55,7 +55,7 @@
344         }
345         response = self.client.post('/test_admin/admin/admin_views/section/add/', post_data)
346         self.failUnlessEqual(response.status_code, 302) # redirect somewhere
347-   
348+
349     def testBasicEditPost(self):
350         """
351         A smoke test to ensure POST on edit_view works.
352@@ -96,9 +96,14 @@
353     ct = ContentType.objects.get_for_model(Model)
354     return Permission.objects.get(content_type=ct, codename=perm)
355 
356-class AdminViewPermissionsTest(TestCase):
357-    """Tests for Admin Views Permissions."""
358+class AdminViewPermissionsTest(TransactionTestCase):
359+    """
360+    Tests for Admin Views Permissions.
361 
362+    We need TransactionTestCase here, because some data is lodaed manually
363+    via the ORM, not via fixtures and test.Client is used.
364+    """
365+
366     fixtures = ['admin-views-users.xml']
367 
368     def setUp(self):
369@@ -438,7 +443,7 @@
370         should_contain = """<a href="../../%s/">%s</a>""" % (quote(self.pk), escape(self.pk))
371         self.assertContains(response, should_contain)
372 
373-class SecureViewTest(TestCase):
374+class SecureViewTest(TransactionTestCase):
375     fixtures = ['admin-views-users.xml']
376 
377     def setUp(self):
378@@ -471,28 +476,28 @@
379                      LOGIN_FORM_KEY: 1,
380                      'username': 'joepublic',
381                      'password': 'secret'}
382-   
383+
384     def tearDown(self):
385         self.client.logout()
386-   
387+
388     def test_secure_view_shows_login_if_not_logged_in(self):
389         "Ensure that we see the login form"
390         response = self.client.get('/test_admin/admin/secure-view/' )
391         self.assertTemplateUsed(response, 'admin/login.html')
392-   
393+
394     def test_secure_view_login_successfully_redirects_to_original_url(self):
395         request = self.client.get('/test_admin/admin/secure-view/')
396         self.failUnlessEqual(request.status_code, 200)
397         query_string = "the-answer=42"
398         login = self.client.post('/test_admin/admin/secure-view/', self.super_login, QUERY_STRING = query_string )
399         self.assertRedirects(login, '/test_admin/admin/secure-view/?%s' % query_string)
400-   
401+
402     def test_staff_member_required_decorator_works_as_per_admin_login(self):
403         """
404         Make sure only staff members can log in.
405 
406         Successful posts to the login page will redirect to the orignal url.
407-        Unsuccessfull attempts will continue to render the login page with
408+        Unsuccessfull attempts will continue to render the login page with
409         a 200 status code.
410         """
411         # Super User