Browse Source

Fixed #20550 -- Added ability to preserve test db between runs

Greg Chapple 10 years ago
parent
commit
b7aa7c4ab4

+ 4 - 2
django/contrib/gis/db/backends/postgis/creation.py

@@ -82,8 +82,10 @@ class PostGISCreation(DatabaseCreation):
                 self.connection.ops.quote_name(self.template_postgis),)
                 self.connection.ops.quote_name(self.template_postgis),)
         return ''
         return ''
 
 
-    def _create_test_db(self, verbosity, autoclobber):
+    def _create_test_db(self, verbosity, autoclobber, keepdb=False):
-        test_database_name = super(PostGISCreation, self)._create_test_db(verbosity, autoclobber)
+        test_database_name = super(PostGISCreation, self)._create_test_db(verbosity, autoclobber, keepdb)
+        if keepdb:
+            return test_database_name
         if self.template_postgis is None:
         if self.template_postgis is None:
             # Connect to the test database in order to create the postgis extension
             # Connect to the test database in order to create the postgis extension
             self.connection.close()
             self.connection.close()

+ 7 - 3
django/contrib/gis/db/backends/spatialite/creation.py

@@ -7,7 +7,7 @@ from django.db.backends.sqlite3.creation import DatabaseCreation
 
 
 class SpatiaLiteCreation(DatabaseCreation):
 class SpatiaLiteCreation(DatabaseCreation):
 
 
-    def create_test_db(self, verbosity=1, autoclobber=False):
+    def create_test_db(self, verbosity=1, autoclobber=False, keepdb=False):
         """
         """
         Creates a test database, prompting the user for confirmation if the
         Creates a test database, prompting the user for confirmation if the
         database already exists. Returns the name of the test database created.
         database already exists. Returns the name of the test database created.
@@ -22,11 +22,15 @@ class SpatiaLiteCreation(DatabaseCreation):
 
 
         if verbosity >= 1:
         if verbosity >= 1:
             test_db_repr = ''
             test_db_repr = ''
+            action = 'Creating'
             if verbosity >= 2:
             if verbosity >= 2:
                 test_db_repr = " ('%s')" % test_database_name
                 test_db_repr = " ('%s')" % test_database_name
-            print("Creating test database for alias '%s'%s..." % (self.connection.alias, test_db_repr))
+            if keepdb:
+                action = 'Using existing'
+            print("%s test database for alias '%s'%s..." % (
+                action, self.connection.alias, test_db_repr))
 
 
-        self._create_test_db(verbosity, autoclobber)
+        self._create_test_db(verbosity, autoclobber, keepdb)
 
 
         self.connection.close()
         self.connection.close()
         self.connection.settings_dict["NAME"] = test_database_name
         self.connection.settings_dict["NAME"] = test_database_name

+ 19 - 5
django/db/backends/creation.py

@@ -332,7 +332,7 @@ class BaseDatabaseCreation(object):
             ";",
             ";",
         ]
         ]
 
 
-    def create_test_db(self, verbosity=1, autoclobber=False):
+    def create_test_db(self, verbosity=1, autoclobber=False, keepdb=False):
         """
         """
         Creates a test database, prompting the user for confirmation if the
         Creates a test database, prompting the user for confirmation if the
         database already exists. Returns the name of the test database created.
         database already exists. Returns the name of the test database created.
@@ -344,12 +344,21 @@ class BaseDatabaseCreation(object):
 
 
         if verbosity >= 1:
         if verbosity >= 1:
             test_db_repr = ''
             test_db_repr = ''
+            action = 'Creating'
             if verbosity >= 2:
             if verbosity >= 2:
                 test_db_repr = " ('%s')" % test_database_name
                 test_db_repr = " ('%s')" % test_database_name
-            print("Creating test database for alias '%s'%s..." % (
+            if keepdb:
-                self.connection.alias, test_db_repr))
+                action = "Using existing"
+
+            print("%s test database for alias '%s'%s..." % (
+                action, self.connection.alias, test_db_repr))
 
 
-        self._create_test_db(verbosity, autoclobber)
+        # We could skip this call if keepdb is True, but we instead
+        # give it the keepdb param. This is to handle the case
+        # where the test DB doesn't exist, in which case we need to
+        # create it, then just not destroy it. If we instead skip
+        # this, we will get an exception.
+        self._create_test_db(verbosity, autoclobber, keepdb)
 
 
         self.connection.close()
         self.connection.close()
         settings.DATABASES[self.connection.alias]["NAME"] = test_database_name
         settings.DATABASES[self.connection.alias]["NAME"] = test_database_name
@@ -393,7 +402,7 @@ class BaseDatabaseCreation(object):
             return self.connection.settings_dict['TEST']['NAME']
             return self.connection.settings_dict['TEST']['NAME']
         return TEST_DATABASE_PREFIX + self.connection.settings_dict['NAME']
         return TEST_DATABASE_PREFIX + self.connection.settings_dict['NAME']
 
 
-    def _create_test_db(self, verbosity, autoclobber):
+    def _create_test_db(self, verbosity, autoclobber, keepdb=False):
         """
         """
         Internal implementation - creates the test db tables.
         Internal implementation - creates the test db tables.
         """
         """
@@ -409,6 +418,11 @@ class BaseDatabaseCreation(object):
                 cursor.execute(
                 cursor.execute(
                     "CREATE DATABASE %s %s" % (qn(test_database_name), suffix))
                     "CREATE DATABASE %s %s" % (qn(test_database_name), suffix))
             except Exception as e:
             except Exception as e:
+                # if we want to keep the db, then no need to do any of the below,
+                # just return and skip it all.
+                if keepdb:
+                    return test_database_name
+
                 sys.stderr.write(
                 sys.stderr.write(
                     "Got an error creating the test database: %s\n" % e)
                     "Got an error creating the test database: %s\n" % e)
                 if not autoclobber:
                 if not autoclobber:

+ 5 - 1
django/db/backends/oracle/creation.py

@@ -56,7 +56,7 @@ class DatabaseCreation(BaseDatabaseCreation):
     def __init__(self, connection):
     def __init__(self, connection):
         super(DatabaseCreation, self).__init__(connection)
         super(DatabaseCreation, self).__init__(connection)
 
 
-    def _create_test_db(self, verbosity=1, autoclobber=False):
+    def _create_test_db(self, verbosity=1, autoclobber=False, keepdb=False):
         TEST_NAME = self._test_database_name()
         TEST_NAME = self._test_database_name()
         TEST_USER = self._test_database_user()
         TEST_USER = self._test_database_user()
         TEST_PASSWD = self._test_database_passwd()
         TEST_PASSWD = self._test_database_passwd()
@@ -76,6 +76,10 @@ class DatabaseCreation(BaseDatabaseCreation):
             try:
             try:
                 self._execute_test_db_creation(cursor, parameters, verbosity)
                 self._execute_test_db_creation(cursor, parameters, verbosity)
             except Exception as e:
             except Exception as e:
+                # if we want to keep the db, then no need to do any of the below,
+                # just return and skip it all.
+                if keepdb:
+                    return
                 sys.stderr.write("Got an error creating the test database: %s\n" % e)
                 sys.stderr.write("Got an error creating the test database: %s\n" % e)
                 if not autoclobber:
                 if not autoclobber:
                     confirm = input("It appears the test database, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % TEST_NAME)
                     confirm = input("It appears the test database, %s, already exists. Type 'yes' to delete it, or 'no' to cancel: " % TEST_NAME)

+ 3 - 1
django/db/backends/sqlite3/creation.py

@@ -52,8 +52,10 @@ class DatabaseCreation(BaseDatabaseCreation):
             return test_database_name
             return test_database_name
         return ':memory:'
         return ':memory:'
 
 
-    def _create_test_db(self, verbosity, autoclobber):
+    def _create_test_db(self, verbosity, autoclobber, keepdb=False):
         test_database_name = self._get_test_db_name()
         test_database_name = self._get_test_db_name()
+        if keepdb:
+            return test_database_name
         if test_database_name != ':memory:':
         if test_database_name != ':memory:':
             # Erase the old test database
             # Erase the old test database
             if verbosity >= 1:
             if verbosity >= 1:

+ 9 - 5
django/test/runner.py

@@ -26,10 +26,13 @@ class DiscoverRunner(object):
         make_option('-p', '--pattern', action='store', dest='pattern',
         make_option('-p', '--pattern', action='store', dest='pattern',
             default="test*.py",
             default="test*.py",
             help='The test matching pattern. Defaults to test*.py.'),
             help='The test matching pattern. Defaults to test*.py.'),
+        make_option('-k', '--keepdb', action='store_true', dest='keepdb',
+            default=False,
+            help='Preserve the test DB between runs. Defaults to False'),
     )
     )
 
 
     def __init__(self, pattern=None, top_level=None,
     def __init__(self, pattern=None, top_level=None,
-                 verbosity=1, interactive=True, failfast=False,
+                 verbosity=1, interactive=True, failfast=False, keepdb=False,
                  **kwargs):
                  **kwargs):
 
 
         self.pattern = pattern
         self.pattern = pattern
@@ -38,6 +41,7 @@ class DiscoverRunner(object):
         self.verbosity = verbosity
         self.verbosity = verbosity
         self.interactive = interactive
         self.interactive = interactive
         self.failfast = failfast
         self.failfast = failfast
+        self.keepdb = keepdb
 
 
     def setup_test_environment(self, **kwargs):
     def setup_test_environment(self, **kwargs):
         setup_test_environment()
         setup_test_environment()
@@ -106,7 +110,7 @@ class DiscoverRunner(object):
         return reorder_suite(suite, self.reorder_by)
         return reorder_suite(suite, self.reorder_by)
 
 
     def setup_databases(self, **kwargs):
     def setup_databases(self, **kwargs):
-        return setup_databases(self.verbosity, self.interactive, **kwargs)
+        return setup_databases(self.verbosity, self.interactive, self.keepdb, **kwargs)
 
 
     def run_suite(self, suite, **kwargs):
     def run_suite(self, suite, **kwargs):
         return self.test_runner(
         return self.test_runner(
@@ -120,7 +124,7 @@ class DiscoverRunner(object):
         """
         """
         old_names, mirrors = old_config
         old_names, mirrors = old_config
         for connection, old_name, destroy in old_names:
         for connection, old_name, destroy in old_names:
-            if destroy:
+            if destroy and not self.keepdb:
                 connection.creation.destroy_test_db(old_name, self.verbosity)
                 connection.creation.destroy_test_db(old_name, self.verbosity)
 
 
     def teardown_test_environment(self, **kwargs):
     def teardown_test_environment(self, **kwargs):
@@ -250,7 +254,7 @@ def partition_suite(suite, classes, bins):
                 bins[-1].addTest(test)
                 bins[-1].addTest(test)
 
 
 
 
-def setup_databases(verbosity, interactive, **kwargs):
+def setup_databases(verbosity, interactive, keepdb=False, **kwargs):
     from django.db import connections, DEFAULT_DB_ALIAS
     from django.db import connections, DEFAULT_DB_ALIAS
 
 
     # First pass -- work out which databases actually need to be created,
     # First pass -- work out which databases actually need to be created,
@@ -294,7 +298,7 @@ def setup_databases(verbosity, interactive, **kwargs):
             connection = connections[alias]
             connection = connections[alias]
             if test_db_name is None:
             if test_db_name is None:
                 test_db_name = connection.creation.create_test_db(
                 test_db_name = connection.creation.create_test_db(
-                    verbosity, autoclobber=not interactive)
+                    verbosity, autoclobber=not interactive, keepdb=keepdb)
                 destroy = True
                 destroy = True
             else:
             else:
                 connection.settings_dict['NAME'] = test_db_name
                 connection.settings_dict['NAME'] = test_db_name

+ 11 - 0
docs/ref/django-admin.txt

@@ -1310,6 +1310,17 @@ The ``--liveserver`` option can be used to override the default address where
 the live server (used with :class:`~django.test.LiveServerTestCase`) is
 the live server (used with :class:`~django.test.LiveServerTestCase`) is
 expected to run from. The default value is ``localhost:8081``.
 expected to run from. The default value is ``localhost:8081``.
 
 
+.. django-admin-option:: --keepdb
+
+.. versionadded:: 1.8
+
+The ``--keepdb`` option can be used to preserve the test database between test
+runs. This has the advantage of skipping both the create and destroy actions
+which greatly decreases the time to run tests, especially those in a large
+test suite. If the test database does not exist, it will be created on the first
+run and then preserved for each subsequent run. Any unapplied migrations will also
+be applied to the test database before running the test suite.
+
 testserver <fixture fixture ...>
 testserver <fixture fixture ...>
 --------------------------------
 --------------------------------
 
 

+ 3 - 0
docs/releases/1.8.txt

@@ -189,6 +189,9 @@ Tests
 * The new :meth:`~django.test.SimpleTestCase.assertJSONNotEqual` assertion
 * The new :meth:`~django.test.SimpleTestCase.assertJSONNotEqual` assertion
   allows you to test that two JSON fragments are not equal.
   allows you to test that two JSON fragments are not equal.
 
 
+* Added the ability to preserve the test database by adding the :djadminopt:`--keepdb`
+  flag.
+
 Validators
 Validators
 ^^^^^^^^^^
 ^^^^^^^^^^
 
 

+ 8 - 0
docs/topics/testing/overview.txt

@@ -149,6 +149,14 @@ Tests that require a database (namely, model tests) will not use your "real"
 Regardless of whether the tests pass or fail, the test databases are destroyed
 Regardless of whether the tests pass or fail, the test databases are destroyed
 when all the tests have been executed.
 when all the tests have been executed.
 
 
+.. versionadded:: 1.8
+
+   You can prevent the test databases from being destroyed by adding the
+   :djadminopt:`--keepdb` flag to the test command. This will preserve the test
+   database between runs. If the database does not exist, it will first
+   be created. Any migrations will also be applied in order to keep it
+   up to date.
+
 By default the test databases get their names by prepending ``test_``
 By default the test databases get their names by prepending ``test_``
 to the value of the :setting:`NAME` settings for the databases
 to the value of the :setting:`NAME` settings for the databases
 defined in :setting:`DATABASES`. When using the SQLite database engine
 defined in :setting:`DATABASES`. When using the SQLite database engine

+ 1 - 1
tests/test_runner/tests.py

@@ -310,7 +310,7 @@ class AliasedDatabaseTeardownTest(unittest.TestCase):
         try:
         try:
             destroyed_names = []
             destroyed_names = []
             DatabaseCreation.destroy_test_db = lambda self, old_database_name, verbosity=1: destroyed_names.append(old_database_name)
             DatabaseCreation.destroy_test_db = lambda self, old_database_name, verbosity=1: destroyed_names.append(old_database_name)
-            DatabaseCreation.create_test_db = lambda self, verbosity=1, autoclobber=False: self._get_test_db_name()
+            DatabaseCreation.create_test_db = lambda self, verbosity=1, autoclobber=False, keepdb=False: self._get_test_db_name()
 
 
             db.connections = db.ConnectionHandler({
             db.connections = db.ConnectionHandler({
                 'default': {
                 'default': {