Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

[1.2.X] Fixed #14799 -- Provided a full solution for test database cr…

…eation order problems.

Backport of r14822, r14823 and r14824 from trunk.

git-svn-id: http://code.djangoproject.com/svn/django/branches/releases/1.2.X@14825 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit b0d9eaaa6a2cd3670bc005a5a95a44790794ccad 1 parent 778782e
Russell Keith-Magee authored December 05, 2010
64  django/test/simple.py
@@ -3,11 +3,18 @@
3 3
 import unittest
4 4
 
5 5
 from django.conf import settings
  6
+from django.core.exceptions import ImproperlyConfigured
6 7
 from django.db.models import get_app, get_apps
7 8
 from django.test import _doctest as doctest
8 9
 from django.test.utils import setup_test_environment, teardown_test_environment
9 10
 from django.test.testcases import OutputChecker, DocTestRunner, TestCase
10 11
 
  12
+
  13
+try:
  14
+    all
  15
+except NameError:
  16
+    from django.utils.itercompat import all
  17
+
11 18
 # The module name for tests outside models.py
12 19
 TEST_MODULE = 'tests'
13 20
 
@@ -222,6 +229,40 @@ def reorder_suite(suite, classes):
222 229
         bins[0].addTests(bins[i+1])
223 230
     return bins[0]
224 231
 
  232
+def dependency_ordered(test_databases, dependencies):
  233
+    """Reorder test_databases into an order that honors the dependencies
  234
+    described in TEST_DEPENDENCIES.
  235
+    """
  236
+    ordered_test_databases = []
  237
+    resolved_databases = set()
  238
+    while test_databases:
  239
+        changed = False
  240
+        deferred = []
  241
+
  242
+        while test_databases:
  243
+            signature, aliases = test_databases.pop()
  244
+            dependencies_satisfied = True
  245
+            for alias in aliases:
  246
+                if alias in dependencies:
  247
+                    if all(a in resolved_databases for a in dependencies[alias]):
  248
+                        # all dependencies for this alias are satisfied
  249
+                        dependencies.pop(alias)
  250
+                        resolved_databases.add(alias)
  251
+                    else:
  252
+                        dependencies_satisfied = False
  253
+                else:
  254
+                    resolved_databases.add(alias)
  255
+
  256
+            if dependencies_satisfied:
  257
+                ordered_test_databases.append((signature, aliases))
  258
+                changed = True
  259
+            else:
  260
+                deferred.append((signature, aliases))
  261
+
  262
+        if not changed:
  263
+            raise ImproperlyConfigured("Circular dependency in TEST_DEPENDENCIES")
  264
+        test_databases = deferred
  265
+    return ordered_test_databases
225 266
 
226 267
 class DjangoTestSuiteRunner(object):
227 268
     def __init__(self, verbosity=1, interactive=True, failfast=True, **kwargs):
@@ -260,6 +301,7 @@ def setup_databases(self, **kwargs):
260 301
         # and which ones are test mirrors or duplicate entries in DATABASES
261 302
         mirrored_aliases = {}
262 303
         test_databases = {}
  304
+        dependencies = {}
263 305
         for alias in connections:
264 306
             connection = connections[alias]
265 307
             if connection.settings_dict['TEST_MIRROR']:
@@ -276,21 +318,17 @@ def setup_databases(self, **kwargs):
276 318
                         connection.settings_dict['ENGINE'],
277 319
                         connection.settings_dict['NAME'],
278 320
                     ), []).append(alias)
279  
-        
280  
-        # Re-order the list of databases to create, making sure the default
281  
-        # database is first. Otherwise, creation order is semi-random (i.e. 
282  
-        # dict ordering dependent).
283  
-        dbs_to_create = []
284  
-        for dbinfo, aliases in test_databases.items():
285  
-            if DEFAULT_DB_ALIAS in aliases:
286  
-                dbs_to_create.insert(0, (dbinfo, aliases))
287  
-            else:
288  
-                dbs_to_create.append((dbinfo, aliases))
289  
-                
290  
-        # Final pass -- actually create the databases.
  321
+
  322
+                if 'TEST_DEPENDENCIES' in connection.settings_dict:
  323
+                    dependencies[alias] = connection.settings_dict['TEST_DEPENDENCIES']
  324
+                else:
  325
+                    if alias != 'default':
  326
+                        dependencies[alias] = connection.settings_dict.get('TEST_DEPENDENCIES', ['default'])
  327
+
  328
+        # Second pass -- actually create the databases.
291 329
         old_names = []
292 330
         mirrors = []
293  
-        for (host, port, engine, db_name), aliases in dbs_to_create:
  331
+        for (host, port, engine, db_name), aliases in dependency_ordered(test_databases.items(), dependencies):
294 332
             # Actually create the database for the first connection
295 333
             connection = connections[aliases[0]]
296 334
             old_names.append((connection, db_name, True))
14  docs/ref/settings.txt
@@ -380,6 +380,20 @@ Only supported for the ``mysql`` backend (see the `MySQL manual`_ for details).
380 380
 
381 381
 .. _MySQL manual: MySQL_
382 382
 
  383
+.. setting:: TEST_DEPENDENCIES
  384
+
  385
+TEST_DEPENDENCIES
  386
+~~~~~~~~~~~~~~~~~
  387
+
  388
+.. versionadded:: 1.2.4
  389
+
  390
+Default: ``['default']``, for all databases other than ``default``,
  391
+which has no dependencies.
  392
+
  393
+The creation-order dependencies of the database. See the documentation
  394
+on :ref:`controlling the creation order of test databases
  395
+<topics-testing-creation-dependencies>` for details.
  396
+
383 397
 .. setting:: TEST_MIRROR
384 398
 
385 399
 TEST_MIRROR
39  docs/releases/1.2.4.txt
... ...
@@ -0,0 +1,39 @@
  1
+==========================
  2
+Django 1.2.4 release notes
  3
+==========================
  4
+
  5
+Welcome to Django 1.2.4!
  6
+
  7
+This is the fourth "bugfix" release in the Django 1.2 series,
  8
+improving the stability and performance of the Django 1.2 codebase.
  9
+
  10
+Django 1.2.4 maintains backwards compatibility with Django
  11
+1.2.3, but contain a number of fixes and other
  12
+improvements. Django 1.2.4 is a recommended upgrade for any
  13
+development or deployment currently using or targeting Django 1.2.
  14
+
  15
+For full details on the new features, backwards incompatibilities, and
  16
+deprecated features in the 1.2 branch, see the :doc:`/releases/1.2`.
  17
+
  18
+One new feature
  19
+===============
  20
+
  21
+Ordinarily, a point release would not include new features, but in the
  22
+case of Django 1.2.4, we have made an exception to this rule.
  23
+
  24
+One of the bugs fixed in Django 1.2.4 involves a set of
  25
+circumstances whereby a running a test suite on a multiple database
  26
+configuration could cause the original source database (i.e., the
  27
+actual production database) to be dropped, causing catastrophic loss
  28
+of data. In order to provide a fix for this problem, it was necessary
  29
+to introduce a new setting -- :setting:`TEST_DEPENDENCIES` -- that
  30
+allows you to define any creation order dependencies in your database
  31
+configuration.
  32
+
  33
+Most users -- even users with multiple-database configurations -- need
  34
+not be concerned about the data loss bug, or the manual configuration of
  35
+:setting:`TEST_DEPENDENCIES`. See the `original problem report`_
  36
+documentation on :ref:`controlling the creation order of test
  37
+databases <topics-testing-creation-dependencies>` for details.
  38
+
  39
+.. _original problem report: http://code.djangoproject.com/ticket/14415
1  docs/releases/index.txt
@@ -19,6 +19,7 @@ Final releases
19 19
 .. toctree::
20 20
    :maxdepth: 1
21 21
 
  22
+   1.2.4
22 23
    1.2.2
23 24
    1.2
24 25
 
47  docs/topics/testing.txt
@@ -422,6 +422,53 @@ will be redirected to point at ``default``. As a result, writes to
422 422
 the same database, not because there is data replication between the
423 423
 two databases.
424 424
 
  425
+.. _topics-testing-creation-dependencies:
  426
+
  427
+Controlling creation order for test databases
  428
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
  429
+
  430
+.. versionadded:: 1.2.4
  431
+
  432
+By default, Django will always create the ``default`` database first.
  433
+However, no guarantees are made on the creation order of any other
  434
+databases in your test setup.
  435
+
  436
+If your database configuration requires a specific creation order, you
  437
+can specify the dependencies that exist using the
  438
+:setting:`TEST_DEPENDENCIES` setting. Consider the following
  439
+(simplified) example database configuration::
  440
+
  441
+    DATABASES = {
  442
+        'default': {
  443
+             # ... db settings
  444
+             'TEST_DEPENDENCIES': ['diamonds']
  445
+        },
  446
+        'diamonds': {
  447
+            # ... db settings
  448
+        },
  449
+        'clubs': {
  450
+            # ... db settings
  451
+            'TEST_DEPENDENCIES': ['diamonds']
  452
+        },
  453
+        'spades': {
  454
+            # ... db settings
  455
+            'TEST_DEPENDENCIES': ['diamonds','hearts']
  456
+        },
  457
+        'hearts': {
  458
+            # ... db settings
  459
+            'TEST_DEPENDENCIES': ['diamonds','clubs']
  460
+        }
  461
+    }
  462
+
  463
+Under this configuration, the ``diamonds`` database will be created first,
  464
+as it is the only database alias without dependencies. The ``default``` and
  465
+``clubs`` alias will be created next (although the order of creation of this
  466
+pair is not guaranteed); then ``hearts``; and finally ``spades``.
  467
+
  468
+If there are any circular dependencies in the
  469
+:setting:`TEST_DEPENDENCIES` definition, an ``ImproperlyConfigured``
  470
+exception will be raised.
  471
+
425 472
 Other test conditions
426 473
 ---------------------
427 474
 
91  tests/regressiontests/test_runner/tests.py
@@ -4,6 +4,7 @@
4 4
 import StringIO
5 5
 import unittest
6 6
 import django
  7
+from django.core.exceptions import ImproperlyConfigured
7 8
 from django.test import simple
8 9
 
9 10
 class DjangoTestRunnerTests(unittest.TestCase):
@@ -27,3 +28,93 @@ def runTest(self):
27 28
         result = dtr.run(suite)
28 29
         self.assertEqual(1, result.testsRun)
29 30
         self.assertEqual(1, len(result.failures))
  31
+
  32
+class DependencyOrderingTests(unittest.TestCase):
  33
+
  34
+    def test_simple_dependencies(self):
  35
+        raw = [
  36
+            ('s1', ['alpha']),
  37
+            ('s2', ['bravo']),
  38
+            ('s3', ['charlie']),
  39
+        ]
  40
+        dependencies = {
  41
+            'alpha': ['charlie'],
  42
+            'bravo': ['charlie'],
  43
+        }
  44
+
  45
+        ordered = simple.dependency_ordered(raw, dependencies=dependencies)
  46
+        ordered_sigs = [sig for sig,aliases in ordered]
  47
+
  48
+        self.assertIn('s1', ordered_sigs)
  49
+        self.assertIn('s2', ordered_sigs)
  50
+        self.assertIn('s3', ordered_sigs)
  51
+        self.assertLess(ordered_sigs.index('s3'), ordered_sigs.index('s1'))
  52
+        self.assertLess(ordered_sigs.index('s3'), ordered_sigs.index('s2'))
  53
+
  54
+    def test_chained_dependencies(self):
  55
+        raw = [
  56
+            ('s1', ['alpha']),
  57
+            ('s2', ['bravo']),
  58
+            ('s3', ['charlie']),
  59
+        ]
  60
+        dependencies = {
  61
+            'alpha': ['bravo'],
  62
+            'bravo': ['charlie'],
  63
+        }
  64
+
  65
+        ordered = simple.dependency_ordered(raw, dependencies=dependencies)
  66
+        ordered_sigs = [sig for sig,aliases in ordered]
  67
+
  68
+        self.assertIn('s1', ordered_sigs)
  69
+        self.assertIn('s2', ordered_sigs)
  70
+        self.assertIn('s3', ordered_sigs)
  71
+
  72
+        # Explicit dependencies
  73
+        self.assertLess(ordered_sigs.index('s2'), ordered_sigs.index('s1'))
  74
+        self.assertLess(ordered_sigs.index('s3'), ordered_sigs.index('s2'))
  75
+
  76
+        # Implied dependencies
  77
+        self.assertLess(ordered_sigs.index('s3'), ordered_sigs.index('s1'))
  78
+
  79
+    def test_multiple_dependencies(self):
  80
+        raw = [
  81
+            ('s1', ['alpha']),
  82
+            ('s2', ['bravo']),
  83
+            ('s3', ['charlie']),
  84
+            ('s4', ['delta']),
  85
+        ]
  86
+        dependencies = {
  87
+            'alpha': ['bravo','delta'],
  88
+            'bravo': ['charlie'],
  89
+            'delta': ['charlie'],
  90
+        }
  91
+
  92
+        ordered = simple.dependency_ordered(raw, dependencies=dependencies)
  93
+        ordered_sigs = [sig for sig,aliases in ordered]
  94
+
  95
+        self.assertIn('s1', ordered_sigs)
  96
+        self.assertIn('s2', ordered_sigs)
  97
+        self.assertIn('s3', ordered_sigs)
  98
+        self.assertIn('s4', ordered_sigs)
  99
+
  100
+        # Explicit dependencies
  101
+        self.assertLess(ordered_sigs.index('s2'), ordered_sigs.index('s1'))
  102
+        self.assertLess(ordered_sigs.index('s4'), ordered_sigs.index('s1'))
  103
+        self.assertLess(ordered_sigs.index('s3'), ordered_sigs.index('s2'))
  104
+        self.assertLess(ordered_sigs.index('s3'), ordered_sigs.index('s4'))
  105
+
  106
+        # Implicit dependencies
  107
+        self.assertLess(ordered_sigs.index('s3'), ordered_sigs.index('s1'))
  108
+
  109
+    def test_circular_dependencies(self):
  110
+        raw = [
  111
+            ('s1', ['alpha']),
  112
+            ('s2', ['bravo']),
  113
+        ]
  114
+        dependencies = {
  115
+            'bravo': ['alpha'],
  116
+            'alpha': ['bravo'],
  117
+        }
  118
+
  119
+        self.assertRaises(ImproperlyConfigured, simple.dependency_ordered, raw, dependencies=dependencies)
  120
+

0 notes on commit b0d9eaa

Please sign in to comment.
Something went wrong with that request. Please try again.