Browse Source

Fixed #28478 -- Make DiscoverRunner skip creating unused test databases.

SimpleTestCase.databases makes it possible to determine the set of
databases required to run the discovered tests.
Simon Charette 6 years ago
parent
commit
41e73de39d

+ 26 - 1
django/test/runner.py

@@ -391,6 +391,9 @@ class ParallelTestSuite(unittest.TestSuite):
 
         return result
 
+    def __iter__(self):
+        return iter(self.subsuites)
+
 
 class DiscoverRunner:
     """A Django test runner that uses unittest2 test discovery."""
@@ -587,6 +590,27 @@ class DiscoverRunner:
     def suite_result(self, suite, result, **kwargs):
         return len(result.failures) + len(result.errors)
 
+    def _get_databases(self, suite):
+        databases = set()
+        for test in suite:
+            if isinstance(test, unittest.TestCase):
+                test_databases = getattr(test, 'databases', None)
+                if test_databases == '__all__':
+                    return set(connections)
+                if test_databases:
+                    databases.update(test_databases)
+            else:
+                databases.update(self._get_databases(test))
+        return databases
+
+    def get_databases(self, suite):
+        databases = self._get_databases(suite)
+        if self.verbosity >= 2:
+            unused_databases = [alias for alias in connections if alias not in databases]
+            if unused_databases:
+                print('Skipping setup of unused database(s): %s.' % ', '.join(sorted(unused_databases)))
+        return databases
+
     def run_tests(self, test_labels, extra_tests=None, **kwargs):
         """
         Run the unit tests for all the test labels in the provided list.
@@ -601,7 +625,8 @@ class DiscoverRunner:
         """
         self.setup_test_environment()
         suite = self.build_suite(test_labels, extra_tests)
-        old_config = self.setup_databases()
+        databases = self.get_databases(suite)
+        old_config = self.setup_databases(aliases=databases)
         run_failed = False
         try:
             self.run_checks()

+ 6 - 4
django/test/utils.py

@@ -152,9 +152,9 @@ def teardown_test_environment():
     del mail.outbox
 
 
-def setup_databases(verbosity, interactive, keepdb=False, debug_sql=False, parallel=0, **kwargs):
+def setup_databases(verbosity, interactive, keepdb=False, debug_sql=False, parallel=0, aliases=None, **kwargs):
     """Create the test databases."""
-    test_databases, mirrored_aliases = get_unique_databases_and_mirrors()
+    test_databases, mirrored_aliases = get_unique_databases_and_mirrors(aliases)
 
     old_names = []
 
@@ -238,7 +238,7 @@ def dependency_ordered(test_databases, dependencies):
     return ordered_test_databases
 
 
-def get_unique_databases_and_mirrors():
+def get_unique_databases_and_mirrors(aliases=None):
     """
     Figure out which databases actually need to be created.
 
@@ -250,6 +250,8 @@ def get_unique_databases_and_mirrors():
                       where all aliases share the same underlying database.
     - mirrored_aliases: mapping of mirror aliases to original aliases.
     """
+    if aliases is None:
+        aliases = connections
     mirrored_aliases = {}
     test_databases = {}
     dependencies = {}
@@ -262,7 +264,7 @@ def get_unique_databases_and_mirrors():
         if test_settings['MIRROR']:
             # If the database is marked as a test mirror, save the alias.
             mirrored_aliases[alias] = test_settings['MIRROR']
-        else:
+        elif alias in aliases:
             # Store a tuple with DB parameters that uniquely identify it.
             # If we have two aliases with the same values for that tuple,
             # we only need to create the test database once.

+ 3 - 0
docs/releases/2.2.txt

@@ -293,6 +293,9 @@ Tests
   for older versions of SQLite because they would require expensive table
   introspection there.
 
+* :class:`~django.test.runner.DiscoverRunner` now skips the setup of databases
+  not :ref:`referenced by tests<testing-multi-db>`.
+
 URLs
 ~~~~
 

+ 9 - 1
docs/topics/testing/advanced.txt

@@ -614,7 +614,7 @@ utility methods in the ``django.test.utils`` module.
     Performs global post-test teardown, such as removing instrumentation from
     the template system and restoring normal email services.
 
-.. function:: setup_databases(verbosity, interactive, keepdb=False, debug_sql=False, parallel=0, **kwargs)
+.. function:: setup_databases(verbosity, interactive, keepdb=False, debug_sql=False, parallel=0, aliases=None, **kwargs)
 
     Creates the test databases.
 
@@ -622,6 +622,14 @@ utility methods in the ``django.test.utils`` module.
     that have been made. This data will be provided to the
     :func:`teardown_databases` function at the conclusion of testing.
 
+    The ``aliases`` argument determines which :setting:`DATABASES` aliases test
+    databases should be setup for. If it's not provided, it defaults to all of
+    :setting:`DATABASES` aliases.
+
+    .. versionadded:: 2.2
+
+        The ``aliases`` argument was added.
+
 .. function:: teardown_databases(old_config, parallel=0, keepdb=False)
 
     Destroys the test databases, restoring pre-test conditions.

+ 8 - 7
docs/topics/testing/tools.txt

@@ -1134,13 +1134,14 @@ Multi-database support
 .. versionadded:: 2.2
 
 Django sets up a test database corresponding to every database that is
-defined in the :setting:`DATABASES` definition in your settings
-file. However, a big part of the time taken to run a Django TestCase
-is consumed by the call to ``flush`` that ensures that you have a
-clean database at the start of each test run. If you have multiple
-databases, multiple flushes are required (one for each database),
-which can be a time consuming activity -- especially if your tests
-don't need to test multi-database activity.
+defined in the :setting:`DATABASES` definition in your settings and referred to
+by at least one test through ``databases``.
+
+However, a big part of the time taken to run a Django ``TestCase`` is consumed
+by the call to ``flush`` that ensures that you have a clean database at the
+start of each test run. If you have multiple databases, multiple flushes are
+required (one for each database), which can be a time consuming activity --
+especially if your tests don't need to test multi-database activity.
 
 As an optimization, Django only flushes the ``default`` database at
 the start of each test run. If your setup contains multiple databases,

+ 45 - 0
tests/test_runner/test_discover_runner.py

@@ -3,6 +3,7 @@ from argparse import ArgumentParser
 from contextlib import contextmanager
 from unittest import TestSuite, TextTestRunner, defaultTestLoader
 
+from django.db import connections
 from django.test import SimpleTestCase
 from django.test.runner import DiscoverRunner
 from django.test.utils import captured_stdout
@@ -223,3 +224,47 @@ class DiscoverRunnerTests(SimpleTestCase):
         with captured_stdout() as stdout:
             runner.build_suite(['test_runner_apps.tagged.tests'])
             self.assertIn('Excluding test tag(s): bar, foo.\n', stdout.getvalue())
+
+
+class DiscoverRunnerGetDatabasesTests(SimpleTestCase):
+    runner = DiscoverRunner(verbosity=2)
+    skip_msg = 'Skipping setup of unused database(s): '
+
+    def get_databases(self, test_labels):
+        suite = self.runner.build_suite(test_labels)
+        with captured_stdout() as stdout:
+            databases = self.runner.get_databases(suite)
+        return databases, stdout.getvalue()
+
+    def test_mixed(self):
+        databases, output = self.get_databases(['test_runner_apps.databases.tests'])
+        self.assertEqual(databases, set(connections))
+        self.assertNotIn(self.skip_msg, output)
+
+    def test_all(self):
+        databases, output = self.get_databases(['test_runner_apps.databases.tests.AllDatabasesTests'])
+        self.assertEqual(databases, set(connections))
+        self.assertNotIn(self.skip_msg, output)
+
+    def test_default_and_other(self):
+        databases, output = self.get_databases([
+            'test_runner_apps.databases.tests.DefaultDatabaseTests',
+            'test_runner_apps.databases.tests.OtherDatabaseTests',
+        ])
+        self.assertEqual(databases, set(connections))
+        self.assertNotIn(self.skip_msg, output)
+
+    def test_default_only(self):
+        databases, output = self.get_databases(['test_runner_apps.databases.tests.DefaultDatabaseTests'])
+        self.assertEqual(databases, {'default'})
+        self.assertIn(self.skip_msg + 'other', output)
+
+    def test_other_only(self):
+        databases, output = self.get_databases(['test_runner_apps.databases.tests.OtherDatabaseTests'])
+        self.assertEqual(databases, {'other'})
+        self.assertIn(self.skip_msg + 'default', output)
+
+    def test_no_databases_required(self):
+        databases, output = self.get_databases(['test_runner_apps.databases.tests.NoDatabaseTests'])
+        self.assertEqual(databases, set())
+        self.assertIn(self.skip_msg + 'default, other', output)

+ 0 - 0
tests/test_runner_apps/databases/__init__.py


+ 18 - 0
tests/test_runner_apps/databases/tests.py

@@ -0,0 +1,18 @@
+import unittest
+
+
+class NoDatabaseTests(unittest.TestCase):
+    def test_nothing(self):
+        pass
+
+
+class DefaultDatabaseTests(NoDatabaseTests):
+    databases = {'default'}
+
+
+class OtherDatabaseTests(NoDatabaseTests):
+    databases = {'other'}
+
+
+class AllDatabasesTests(NoDatabaseTests):
+    databases = '__all__'