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
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))
else:
dbs_to_create.append((dbinfo, aliases))
if 'TEST_DEPENDENCIES' in connection.settings_dict:
dependencies[alias] = connection.settings_dict['TEST_DEPENDENCIES']
else:
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))

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
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
---------------------

View File

@ -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)