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.
This commit is contained in:
Simon Charette 2018-07-12 00:14:24 -04:00 committed by Tim Graham
parent 8c775391b7
commit 41e73de39d
8 changed files with 115 additions and 13 deletions

View File

@ -391,6 +391,9 @@ class ParallelTestSuite(unittest.TestSuite):
return result return result
def __iter__(self):
return iter(self.subsuites)
class DiscoverRunner: class DiscoverRunner:
"""A Django test runner that uses unittest2 test discovery.""" """A Django test runner that uses unittest2 test discovery."""
@ -587,6 +590,27 @@ class DiscoverRunner:
def suite_result(self, suite, result, **kwargs): def suite_result(self, suite, result, **kwargs):
return len(result.failures) + len(result.errors) 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): def run_tests(self, test_labels, extra_tests=None, **kwargs):
""" """
Run the unit tests for all the test labels in the provided list. Run the unit tests for all the test labels in the provided list.
@ -601,7 +625,8 @@ class DiscoverRunner:
""" """
self.setup_test_environment() self.setup_test_environment()
suite = self.build_suite(test_labels, extra_tests) 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 run_failed = False
try: try:
self.run_checks() self.run_checks()

View File

@ -152,9 +152,9 @@ def teardown_test_environment():
del mail.outbox 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.""" """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 = [] old_names = []
@ -238,7 +238,7 @@ def dependency_ordered(test_databases, dependencies):
return ordered_test_databases 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. 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. where all aliases share the same underlying database.
- mirrored_aliases: mapping of mirror aliases to original aliases. - mirrored_aliases: mapping of mirror aliases to original aliases.
""" """
if aliases is None:
aliases = connections
mirrored_aliases = {} mirrored_aliases = {}
test_databases = {} test_databases = {}
dependencies = {} dependencies = {}
@ -262,7 +264,7 @@ def get_unique_databases_and_mirrors():
if test_settings['MIRROR']: if test_settings['MIRROR']:
# If the database is marked as a test mirror, save the alias. # If the database is marked as a test mirror, save the alias.
mirrored_aliases[alias] = test_settings['MIRROR'] mirrored_aliases[alias] = test_settings['MIRROR']
else: elif alias in aliases:
# Store a tuple with DB parameters that uniquely identify it. # Store a tuple with DB parameters that uniquely identify it.
# If we have two aliases with the same values for that tuple, # If we have two aliases with the same values for that tuple,
# we only need to create the test database once. # we only need to create the test database once.

View File

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

View File

@ -614,7 +614,7 @@ utility methods in the ``django.test.utils`` module.
Performs global post-test teardown, such as removing instrumentation from Performs global post-test teardown, such as removing instrumentation from
the template system and restoring normal email services. 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. 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 that have been made. This data will be provided to the
:func:`teardown_databases` function at the conclusion of testing. :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) .. function:: teardown_databases(old_config, parallel=0, keepdb=False)
Destroys the test databases, restoring pre-test conditions. Destroys the test databases, restoring pre-test conditions.

View File

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

View File

@ -3,6 +3,7 @@ from argparse import ArgumentParser
from contextlib import contextmanager from contextlib import contextmanager
from unittest import TestSuite, TextTestRunner, defaultTestLoader from unittest import TestSuite, TextTestRunner, defaultTestLoader
from django.db import connections
from django.test import SimpleTestCase from django.test import SimpleTestCase
from django.test.runner import DiscoverRunner from django.test.runner import DiscoverRunner
from django.test.utils import captured_stdout from django.test.utils import captured_stdout
@ -223,3 +224,47 @@ class DiscoverRunnerTests(SimpleTestCase):
with captured_stdout() as stdout: with captured_stdout() as stdout:
runner.build_suite(['test_runner_apps.tagged.tests']) runner.build_suite(['test_runner_apps.tagged.tests'])
self.assertIn('Excluding test tag(s): bar, foo.\n', stdout.getvalue()) 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)

View File

@ -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__'