Commit b11c21d6 authored by Russell Keith-Magee's avatar Russell Keith-Magee
Browse files

Fixed #14799 -- Provided a full solution for test database creation order problems.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@14822 bcc190cf-cafb-0310-a4f2-bffc1f526a37
parent 111ed019
Loading
Loading
Loading
Loading
+53 −13
Original line number Diff line number Diff line
@@ -2,12 +2,21 @@ import sys
import signal

from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.db.models import get_app, get_apps
from django.test import _doctest as doctest
from django.test.utils import setup_test_environment, teardown_test_environment
from django.test.testcases import OutputChecker, DocTestRunner, TestCase
from django.utils import unittest

try:
    all
except NameError:
    from django.utils.itercompat import all


__all__ = ('DjangoTestRunner', 'DjangoTestSuiteRunner', 'run_tests')

# The module name for tests outside models.py
TEST_MODULE = 'tests'

@@ -183,6 +192,40 @@ def reorder_suite(suite, classes):
        bins[0].addTests(bins[i+1])
    return bins[0]

def dependency_ordered(test_databases, dependencies):
    """Reorder test_databases into an order that honors the dependencies
    described in TEST_DEPENDENCIES.
    """
    ordered_test_databases = []
    resolved_databases = set()
    while test_databases:
        changed = False
        deferred = []

        while test_databases:
            signature, aliases = test_databases.pop()
            dependencies_satisfied = True
            for alias in aliases:
                if alias in dependencies:
                    if all(a in resolved_databases for a in dependencies[alias]):
                        # all dependencies for this alias are satisfied
                        dependencies.pop(alias)
                        resolved_databases.add(alias)
                    else:
                        dependencies_satisfied = False
                else:
                    resolved_databases.add(alias)

            if dependencies_satisfied:
                ordered_test_databases.append((signature, aliases))
                changed = True
            else:
                deferred.append((signature, aliases))

        if not changed:
            raise ImproperlyConfigured("Circular dependency in TEST_DEPENDENCIES")
        test_databases = deferred
    return ordered_test_databases

class DjangoTestSuiteRunner(object):
    def __init__(self, verbosity=1, interactive=True, failfast=True, **kwargs):
@@ -222,6 +265,7 @@ class DjangoTestSuiteRunner(object):
        # and which ones are test mirrors or duplicate entries in DATABASES
        mirrored_aliases = {}
        test_databases = {}
        dependencies = {}
        for alias in connections:
            connection = connections[alias]
            if connection.settings_dict['TEST_MIRROR']:
@@ -239,20 +283,16 @@ class DjangoTestSuiteRunner(object):
                        connection.settings_dict['NAME'],
                    ), []).append(alias)

        # Re-order the list of databases to create, making sure the default
        # database is first. Otherwise, creation order is semi-random (i.e. 
        # dict ordering dependent).
        dbs_to_create = []
        for dbinfo, aliases in test_databases.items():
            if DEFAULT_DB_ALIAS in aliases:
                dbs_to_create.insert(0, (dbinfo, aliases))
                if 'TEST_DEPENDENCIES' in connection.settings_dict:
                    dependencies[alias] = connection.settings_dict['TEST_DEPENDENCIES']
                else:
                dbs_to_create.append((dbinfo, aliases))
                    if alias != 'default':
                        dependencies[alias] = connection.settings_dict.get('TEST_DEPENDENCIES', ['default'])

        # Final pass -- actually create the databases.
        # Second pass -- actually create the databases.
        old_names = []
        mirrors = []
        for (host, port, engine, db_name), aliases in dbs_to_create:
        for (host, port, engine, db_name), aliases in dependency_ordered(test_databases.items(), dependencies):
            # Actually create the database for the first connection
            connection = connections[aliases[0]]
            old_names.append((connection, db_name, True))
+47 −0
Original line number Diff line number Diff line
@@ -454,6 +454,53 @@ will be redirected to point at ``default``. As a result, writes to
the same database, not because there is data replication between the
two databases.

.. _topics-testing-creation-dependencies:

Controlling creation order for test databases
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

.. versionadded:: 1.3

By default, Django will always create the ``default`` database first.
However, no guarantees are made on the creation order of any other
databases in your test setup.

If your database configuration requires a specific creation order, you
can specify the dependencies that exist using the
:setting:`TEST_DEPENDENCIES` setting. Consider the following
(simplified) example database configuration::

    DATABASES = {
        'default': {
             # ... db settings
             TEST_DEPENDENCIES = ['diamonds']
        },
        'diamonds': {
            # ... db settings
        }
        'clubs': {
            # ... db settings
            TEST_DEPENDENCIES = ['diamonds']
        }
        'spades': {
            # ... db settings
            TEST_DEPENDENCIES = ['diamonds','hearts']
        }
        'hearts': {
            # ... db settings
            TEST_DEPENDENCIES = ['diamonds','clubs']
        }
    }

Under this configuration, the ``diamonds`` database will be created first,
as it is the only database alias without dependencies. The ``default``` and
``clubs`` alias will be created next (although the order of creation of this
pair is not guaranteed); then ``hearts``; and finally ``spades``.

If there are any circular dependencies in the
:setting:`TEST_DEPENDENCIES` definition, an ``ImproperlyConfigured``
exception will be raised.

Other test conditions
---------------------

+91 −0
Original line number Diff line number Diff line
@@ -3,6 +3,7 @@ Tests for django test runner
"""
import StringIO

from django.core.exceptions import ImproperlyConfigured
from django.test import simple
from django.utils import unittest

@@ -27,3 +28,93 @@ class DjangoTestRunnerTests(unittest.TestCase):
        result = dtr.run(suite)
        self.assertEqual(1, result.testsRun)
        self.assertEqual(1, len(result.failures))

class DependencyOrderingTests(unittest.TestCase):

    def test_simple_dependencies(self):
        raw = [
            ('s1', ['alpha']),
            ('s2', ['bravo']),
            ('s3', ['charlie']),
        ]
        dependencies = {
            'alpha': ['charlie'],
            'bravo': ['charlie'],
        }

        ordered = simple.dependency_ordered(raw, dependencies=dependencies)
        ordered_sigs = [sig for sig,aliases in ordered]

        self.assertIn('s1', ordered_sigs)
        self.assertIn('s2', ordered_sigs)
        self.assertIn('s3', ordered_sigs)
        self.assertLess(ordered_sigs.index('s3'), ordered_sigs.index('s1'))
        self.assertLess(ordered_sigs.index('s3'), ordered_sigs.index('s2'))

    def test_chained_dependencies(self):
        raw = [
            ('s1', ['alpha']),
            ('s2', ['bravo']),
            ('s3', ['charlie']),
        ]
        dependencies = {
            'alpha': ['bravo'],
            'bravo': ['charlie'],
        }

        ordered = simple.dependency_ordered(raw, dependencies=dependencies)
        ordered_sigs = [sig for sig,aliases in ordered]

        self.assertIn('s1', ordered_sigs)
        self.assertIn('s2', ordered_sigs)
        self.assertIn('s3', ordered_sigs)

        # Explicit dependencies
        self.assertLess(ordered_sigs.index('s2'), ordered_sigs.index('s1'))
        self.assertLess(ordered_sigs.index('s3'), ordered_sigs.index('s2'))

        # Implied dependencies
        self.assertLess(ordered_sigs.index('s3'), ordered_sigs.index('s1'))

    def test_multiple_dependencies(self):
        raw = [
            ('s1', ['alpha']),
            ('s2', ['bravo']),
            ('s3', ['charlie']),
            ('s4', ['delta']),
        ]
        dependencies = {
            'alpha': ['bravo','delta'],
            'bravo': ['charlie'],
            'delta': ['charlie'],
        }

        ordered = simple.dependency_ordered(raw, dependencies=dependencies)
        ordered_sigs = [sig for sig,aliases in ordered]

        self.assertIn('s1', ordered_sigs)
        self.assertIn('s2', ordered_sigs)
        self.assertIn('s3', ordered_sigs)
        self.assertIn('s4', ordered_sigs)

        # Explicit dependencies
        self.assertLess(ordered_sigs.index('s2'), ordered_sigs.index('s1'))
        self.assertLess(ordered_sigs.index('s4'), ordered_sigs.index('s1'))
        self.assertLess(ordered_sigs.index('s3'), ordered_sigs.index('s2'))
        self.assertLess(ordered_sigs.index('s3'), ordered_sigs.index('s4'))

        # Implicit dependencies
        self.assertLess(ordered_sigs.index('s3'), ordered_sigs.index('s1'))

    def test_circular_dependencies(self):
        raw = [
            ('s1', ['alpha']),
            ('s2', ['bravo']),
        ]
        dependencies = {
            'bravo': ['alpha'],
            'alpha': ['bravo'],
        }

        self.assertRaises(ImproperlyConfigured, simple.dependency_ordered, raw, dependencies=dependencies)