[1.2.X] Fixed #14799 -- Provided a full solution for test database creation 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
This commit is contained in:
parent
778782eac8
commit
b0d9eaaa6a
|
@ -3,11 +3,18 @@ import signal
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
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
|
||||||
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
all
|
||||||
|
except NameError:
|
||||||
|
from django.utils.itercompat import all
|
||||||
|
|
||||||
# The module name for tests outside models.py
|
# The module name for tests outside models.py
|
||||||
TEST_MODULE = 'tests'
|
TEST_MODULE = 'tests'
|
||||||
|
|
||||||
|
@ -222,6 +229,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):
|
||||||
|
@ -260,6 +301,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']:
|
||||||
|
@ -276,21 +318,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))
|
||||||
|
|
|
@ -380,6 +380,20 @@ Only supported for the ``mysql`` backend (see the `MySQL manual`_ for details).
|
||||||
|
|
||||||
.. _MySQL manual: MySQL_
|
.. _MySQL manual: MySQL_
|
||||||
|
|
||||||
|
.. setting:: TEST_DEPENDENCIES
|
||||||
|
|
||||||
|
TEST_DEPENDENCIES
|
||||||
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
.. versionadded:: 1.2.4
|
||||||
|
|
||||||
|
Default: ``['default']``, for all databases other than ``default``,
|
||||||
|
which has no dependencies.
|
||||||
|
|
||||||
|
The creation-order dependencies of the database. See the documentation
|
||||||
|
on :ref:`controlling the creation order of test databases
|
||||||
|
<topics-testing-creation-dependencies>` for details.
|
||||||
|
|
||||||
.. setting:: TEST_MIRROR
|
.. setting:: TEST_MIRROR
|
||||||
|
|
||||||
TEST_MIRROR
|
TEST_MIRROR
|
||||||
|
|
|
@ -0,0 +1,39 @@
|
||||||
|
==========================
|
||||||
|
Django 1.2.4 release notes
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Welcome to Django 1.2.4!
|
||||||
|
|
||||||
|
This is the fourth "bugfix" release in the Django 1.2 series,
|
||||||
|
improving the stability and performance of the Django 1.2 codebase.
|
||||||
|
|
||||||
|
Django 1.2.4 maintains backwards compatibility with Django
|
||||||
|
1.2.3, but contain a number of fixes and other
|
||||||
|
improvements. Django 1.2.4 is a recommended upgrade for any
|
||||||
|
development or deployment currently using or targeting Django 1.2.
|
||||||
|
|
||||||
|
For full details on the new features, backwards incompatibilities, and
|
||||||
|
deprecated features in the 1.2 branch, see the :doc:`/releases/1.2`.
|
||||||
|
|
||||||
|
One new feature
|
||||||
|
===============
|
||||||
|
|
||||||
|
Ordinarily, a point release would not include new features, but in the
|
||||||
|
case of Django 1.2.4, we have made an exception to this rule.
|
||||||
|
|
||||||
|
One of the bugs fixed in Django 1.2.4 involves a set of
|
||||||
|
circumstances whereby a running a test suite on a multiple database
|
||||||
|
configuration could cause the original source database (i.e., the
|
||||||
|
actual production database) to be dropped, causing catastrophic loss
|
||||||
|
of data. In order to provide a fix for this problem, it was necessary
|
||||||
|
to introduce a new setting -- :setting:`TEST_DEPENDENCIES` -- that
|
||||||
|
allows you to define any creation order dependencies in your database
|
||||||
|
configuration.
|
||||||
|
|
||||||
|
Most users -- even users with multiple-database configurations -- need
|
||||||
|
not be concerned about the data loss bug, or the manual configuration of
|
||||||
|
:setting:`TEST_DEPENDENCIES`. See the `original problem report`_
|
||||||
|
documentation on :ref:`controlling the creation order of test
|
||||||
|
databases <topics-testing-creation-dependencies>` for details.
|
||||||
|
|
||||||
|
.. _original problem report: http://code.djangoproject.com/ticket/14415
|
|
@ -19,6 +19,7 @@ Final releases
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:maxdepth: 1
|
:maxdepth: 1
|
||||||
|
|
||||||
|
1.2.4
|
||||||
1.2.2
|
1.2.2
|
||||||
1.2
|
1.2
|
||||||
|
|
||||||
|
|
|
@ -422,6 +422,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.2.4
|
||||||
|
|
||||||
|
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
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ Tests for django test runner
|
||||||
import StringIO
|
import StringIO
|
||||||
import unittest
|
import unittest
|
||||||
import django
|
import django
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.test import simple
|
from django.test import simple
|
||||||
|
|
||||||
class DjangoTestRunnerTests(unittest.TestCase):
|
class DjangoTestRunnerTests(unittest.TestCase):
|
||||||
|
@ -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)
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue