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
This commit is contained in:
Russell Keith-Magee 2010-12-05 00:44:34 +00:00
parent 111ed0195e
commit b11c21d69a
3 changed files with 191 additions and 13 deletions

View File

@ -2,12 +2,21 @@ import sys
import signal import signal
from django.conf import settings from django.conf import settings
from django.core.exceptions import ImproperlyConfigured
from django.db.models import get_app, get_apps from django.db.models import get_app, get_apps
from django.test import _doctest as doctest from django.test import _doctest as doctest
from django.test.utils import setup_test_environment, teardown_test_environment from django.test.utils import setup_test_environment, teardown_test_environment
from django.test.testcases import OutputChecker, DocTestRunner, TestCase from django.test.testcases import OutputChecker, DocTestRunner, TestCase
from django.utils import unittest 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 # The module name for tests outside models.py
TEST_MODULE = 'tests' TEST_MODULE = 'tests'
@ -183,6 +192,40 @@ def reorder_suite(suite, classes):
bins[0].addTests(bins[i+1]) bins[0].addTests(bins[i+1])
return bins[0] 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): class DjangoTestSuiteRunner(object):
def __init__(self, verbosity=1, interactive=True, failfast=True, **kwargs): 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 # and which ones are test mirrors or duplicate entries in DATABASES
mirrored_aliases = {} mirrored_aliases = {}
test_databases = {} test_databases = {}
dependencies = {}
for alias in connections: for alias in connections:
connection = connections[alias] connection = connections[alias]
if connection.settings_dict['TEST_MIRROR']: if connection.settings_dict['TEST_MIRROR']:
@ -238,21 +282,17 @@ class DjangoTestSuiteRunner(object):
connection.settings_dict['ENGINE'], connection.settings_dict['ENGINE'],
connection.settings_dict['NAME'], connection.settings_dict['NAME'],
), []).append(alias) ), []).append(alias)
# Re-order the list of databases to create, making sure the default if 'TEST_DEPENDENCIES' in connection.settings_dict:
# database is first. Otherwise, creation order is semi-random (i.e. dependencies[alias] = connection.settings_dict['TEST_DEPENDENCIES']
# dict ordering dependent). else:
dbs_to_create = [] if alias != 'default':
for dbinfo, aliases in test_databases.items(): dependencies[alias] = connection.settings_dict.get('TEST_DEPENDENCIES', ['default'])
if DEFAULT_DB_ALIAS in aliases:
dbs_to_create.insert(0, (dbinfo, aliases)) # Second pass -- actually create the databases.
else:
dbs_to_create.append((dbinfo, aliases))
# Final pass -- actually create the databases.
old_names = [] old_names = []
mirrors = [] 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 # Actually create the database for the first connection
connection = connections[aliases[0]] connection = connections[aliases[0]]
old_names.append((connection, db_name, True)) old_names.append((connection, db_name, True))

View File

@ -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 the same database, not because there is data replication between the
two databases. 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 Other test conditions
--------------------- ---------------------

View File

@ -3,6 +3,7 @@ Tests for django test runner
""" """
import StringIO import StringIO
from django.core.exceptions import ImproperlyConfigured
from django.test import simple from django.test import simple
from django.utils import unittest from django.utils import unittest
@ -27,3 +28,93 @@ class DjangoTestRunnerTests(unittest.TestCase):
result = dtr.run(suite) result = dtr.run(suite)
self.assertEqual(1, result.testsRun) self.assertEqual(1, result.testsRun)
self.assertEqual(1, len(result.failures)) 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)