diff --git a/django/test/simple.py b/django/test/simple.py index 41d9f72f23..3c94ebc64e 100644 --- a/django/test/simple.py +++ b/django/test/simple.py @@ -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']: @@ -238,21 +282,17 @@ class DjangoTestSuiteRunner(object): connection.settings_dict['ENGINE'], 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)) - - # Final pass -- actually create the databases. + + 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']) + + # 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)) diff --git a/docs/topics/testing.txt b/docs/topics/testing.txt index 8314ca4081..e56e577f3b 100644 --- a/docs/topics/testing.txt +++ b/docs/topics/testing.txt @@ -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 --------------------- diff --git a/tests/regressiontests/test_runner/tests.py b/tests/regressiontests/test_runner/tests.py index 2cc09d6cf3..b34c455a31 100644 --- a/tests/regressiontests/test_runner/tests.py +++ b/tests/regressiontests/test_runner/tests.py @@ -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) +