mirror of https://github.com/django/django.git
Fixed #12118 -- Added shared cache support to SQLite in-memory testing.
This commit is contained in:
parent
fca866763a
commit
8c99b7920e
|
@ -9,6 +9,7 @@ from __future__ import unicode_literals
|
||||||
import datetime
|
import datetime
|
||||||
import decimal
|
import decimal
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
import uuid
|
import uuid
|
||||||
import warnings
|
import warnings
|
||||||
|
|
||||||
|
@ -123,6 +124,14 @@ class DatabaseFeatures(BaseDatabaseFeatures):
|
||||||
def can_release_savepoints(self):
|
def can_release_savepoints(self):
|
||||||
return self.uses_savepoints
|
return self.uses_savepoints
|
||||||
|
|
||||||
|
@cached_property
|
||||||
|
def can_share_in_memory_db(self):
|
||||||
|
return (
|
||||||
|
sys.version_info[:2] >= (3, 4) and
|
||||||
|
Database.__name__ == 'sqlite3.dbapi2' and
|
||||||
|
Database.sqlite_version_info >= (3, 7, 13)
|
||||||
|
)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def supports_stddev(self):
|
def supports_stddev(self):
|
||||||
"""Confirm support for STDDEV and related stats functions
|
"""Confirm support for STDDEV and related stats functions
|
||||||
|
@ -405,6 +414,8 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
RuntimeWarning
|
RuntimeWarning
|
||||||
)
|
)
|
||||||
kwargs.update({'check_same_thread': False})
|
kwargs.update({'check_same_thread': False})
|
||||||
|
if self.features.can_share_in_memory_db:
|
||||||
|
kwargs.update({'uri': True})
|
||||||
return kwargs
|
return kwargs
|
||||||
|
|
||||||
def get_new_connection(self, conn_params):
|
def get_new_connection(self, conn_params):
|
||||||
|
@ -429,7 +440,7 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
# If database is in memory, closing the connection destroys the
|
# If database is in memory, closing the connection destroys the
|
||||||
# database. To prevent accidental data loss, ignore close requests on
|
# database. To prevent accidental data loss, ignore close requests on
|
||||||
# an in-memory db.
|
# an in-memory db.
|
||||||
if self.settings_dict['NAME'] != ":memory:":
|
if not self.is_in_memory_db(self.settings_dict['NAME']):
|
||||||
BaseDatabaseWrapper.close(self)
|
BaseDatabaseWrapper.close(self)
|
||||||
|
|
||||||
def _savepoint_allowed(self):
|
def _savepoint_allowed(self):
|
||||||
|
@ -505,6 +516,9 @@ class DatabaseWrapper(BaseDatabaseWrapper):
|
||||||
"""
|
"""
|
||||||
self.cursor().execute("BEGIN")
|
self.cursor().execute("BEGIN")
|
||||||
|
|
||||||
|
def is_in_memory_db(self, name):
|
||||||
|
return name == ":memory:" or "mode=memory" in name
|
||||||
|
|
||||||
|
|
||||||
FORMAT_QMARK_REGEX = re.compile(r'(?<!%)%s')
|
FORMAT_QMARK_REGEX = re.compile(r'(?<!%)%s')
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.db.backends.creation import BaseDatabaseCreation
|
from django.db.backends.creation import BaseDatabaseCreation
|
||||||
from django.utils.six.moves import input
|
from django.utils.six.moves import input
|
||||||
|
|
||||||
|
@ -51,14 +52,22 @@ class DatabaseCreation(BaseDatabaseCreation):
|
||||||
def _get_test_db_name(self):
|
def _get_test_db_name(self):
|
||||||
test_database_name = self.connection.settings_dict['TEST']['NAME']
|
test_database_name = self.connection.settings_dict['TEST']['NAME']
|
||||||
if test_database_name and test_database_name != ':memory:':
|
if test_database_name and test_database_name != ':memory:':
|
||||||
|
if 'mode=memory' in test_database_name:
|
||||||
|
raise ImproperlyConfigured(
|
||||||
|
"Using `mode=memory` parameter in the database name is not allowed, "
|
||||||
|
"use `:memory:` instead."
|
||||||
|
)
|
||||||
return test_database_name
|
return test_database_name
|
||||||
|
if self.connection.features.can_share_in_memory_db:
|
||||||
|
return 'file:memorydb_%s?mode=memory&cache=shared' % self.connection.alias
|
||||||
return ':memory:'
|
return ':memory:'
|
||||||
|
|
||||||
def _create_test_db(self, verbosity, autoclobber, keepdb=False):
|
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:
|
if keepdb:
|
||||||
return test_database_name
|
return test_database_name
|
||||||
if test_database_name != ':memory:':
|
if not self.connection.is_in_memory_db(test_database_name):
|
||||||
# Erase the old test database
|
# Erase the old test database
|
||||||
if verbosity >= 1:
|
if verbosity >= 1:
|
||||||
print("Destroying old test database '%s'..." % self.connection.alias)
|
print("Destroying old test database '%s'..." % self.connection.alias)
|
||||||
|
@ -80,7 +89,7 @@ class DatabaseCreation(BaseDatabaseCreation):
|
||||||
return test_database_name
|
return test_database_name
|
||||||
|
|
||||||
def _destroy_test_db(self, test_database_name, verbosity):
|
def _destroy_test_db(self, test_database_name, verbosity):
|
||||||
if test_database_name and test_database_name != ":memory:":
|
if test_database_name and not self.connection.is_in_memory_db(test_database_name):
|
||||||
# Remove the SQLite database file
|
# Remove the SQLite database file
|
||||||
os.remove(test_database_name)
|
os.remove(test_database_name)
|
||||||
|
|
||||||
|
@ -92,8 +101,8 @@ class DatabaseCreation(BaseDatabaseCreation):
|
||||||
SQLite since the databases will be distinct despite having the same
|
SQLite since the databases will be distinct despite having the same
|
||||||
TEST NAME. See http://www.sqlite.org/inmemorydb.html
|
TEST NAME. See http://www.sqlite.org/inmemorydb.html
|
||||||
"""
|
"""
|
||||||
test_dbname = self._get_test_db_name()
|
test_database_name = self._get_test_db_name()
|
||||||
sig = [self.connection.settings_dict['NAME']]
|
sig = [self.connection.settings_dict['NAME']]
|
||||||
if test_dbname == ':memory:':
|
if self.connection.is_in_memory_db(test_database_name):
|
||||||
sig.append(self.connection.alias)
|
sig.append(self.connection.alias)
|
||||||
return tuple(sig)
|
return tuple(sig)
|
||||||
|
|
|
@ -1212,8 +1212,7 @@ class LiveServerTestCase(TransactionTestCase):
|
||||||
for conn in connections.all():
|
for conn in connections.all():
|
||||||
# If using in-memory sqlite databases, pass the connections to
|
# If using in-memory sqlite databases, pass the connections to
|
||||||
# the server thread.
|
# the server thread.
|
||||||
if (conn.vendor == 'sqlite'
|
if conn.vendor == 'sqlite' and conn.is_in_memory_db(conn.settings_dict['NAME']):
|
||||||
and conn.settings_dict['NAME'] == ':memory:'):
|
|
||||||
# Explicitly enable thread-shareability for this connection
|
# Explicitly enable thread-shareability for this connection
|
||||||
conn.allow_thread_sharing = True
|
conn.allow_thread_sharing = True
|
||||||
connections_override[conn.alias] = conn
|
connections_override[conn.alias] = conn
|
||||||
|
@ -1267,10 +1266,9 @@ class LiveServerTestCase(TransactionTestCase):
|
||||||
cls.server_thread.terminate()
|
cls.server_thread.terminate()
|
||||||
cls.server_thread.join()
|
cls.server_thread.join()
|
||||||
|
|
||||||
# Restore sqlite connections' non-shareability
|
# Restore sqlite in-memory database connections' non-shareability
|
||||||
for conn in connections.all():
|
for conn in connections.all():
|
||||||
if (conn.vendor == 'sqlite'
|
if conn.vendor == 'sqlite' and conn.is_in_memory_db(conn.settings_dict['NAME']):
|
||||||
and conn.settings_dict['NAME'] == ':memory:'):
|
|
||||||
conn.allow_thread_sharing = False
|
conn.allow_thread_sharing = False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
|
|
@ -573,6 +573,10 @@ Tests
|
||||||
|
|
||||||
* Added test client support for file uploads with file-like objects.
|
* Added test client support for file uploads with file-like objects.
|
||||||
|
|
||||||
|
* A shared cache is now used when testing with a SQLite in-memory database when
|
||||||
|
using Python 3.4+ and SQLite 3.7.13+. This allows sharing the database
|
||||||
|
between threads.
|
||||||
|
|
||||||
Validators
|
Validators
|
||||||
^^^^^^^^^^
|
^^^^^^^^^^
|
||||||
|
|
||||||
|
|
|
@ -185,12 +185,20 @@ control the particular collation used by the test database. See the
|
||||||
:doc:`settings documentation </ref/settings>` for details of these
|
:doc:`settings documentation </ref/settings>` for details of these
|
||||||
and other advanced settings.
|
and other advanced settings.
|
||||||
|
|
||||||
|
If using a SQLite in-memory database with Python 3.4+ and SQLite 3.7.13+,
|
||||||
|
`shared cache <https://www.sqlite.org/sharedcache.html>`_ will be enabled, so
|
||||||
|
you can write tests with ability to share the database between threads.
|
||||||
|
|
||||||
.. versionchanged:: 1.7
|
.. versionchanged:: 1.7
|
||||||
|
|
||||||
The different options in the :setting:`TEST <DATABASE-TEST>` database
|
The different options in the :setting:`TEST <DATABASE-TEST>` database
|
||||||
setting used to be separate options in the database settings dictionary,
|
setting used to be separate options in the database settings dictionary,
|
||||||
prefixed with ``TEST_``.
|
prefixed with ``TEST_``.
|
||||||
|
|
||||||
|
.. versionadded:: 1.8
|
||||||
|
|
||||||
|
The ability to use SQLite with a shared cache as described above was added.
|
||||||
|
|
||||||
.. admonition:: Finding data from your production database when running tests?
|
.. admonition:: Finding data from your production database when running tests?
|
||||||
|
|
||||||
If your code attempts to access the database when its modules are compiled,
|
If your code attempts to access the database when its modules are compiled,
|
||||||
|
|
|
@ -6,6 +6,7 @@ import copy
|
||||||
import datetime
|
import datetime
|
||||||
from decimal import Decimal, Rounded
|
from decimal import Decimal, Rounded
|
||||||
import re
|
import re
|
||||||
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import unittest
|
import unittest
|
||||||
import warnings
|
import warnings
|
||||||
|
@ -1201,3 +1202,21 @@ class DBTestSettingsRenamedTests(IgnoreAllDeprecationWarningsMixin, TestCase):
|
||||||
def test_empty_settings(self):
|
def test_empty_settings(self):
|
||||||
with override_settings(DATABASES=self.db_settings):
|
with override_settings(DATABASES=self.db_settings):
|
||||||
self.handler.prepare_test_settings('default')
|
self.handler.prepare_test_settings('default')
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(connection.vendor == 'sqlite', 'SQLite specific test.')
|
||||||
|
@skipUnlessDBFeature('can_share_in_memory_db')
|
||||||
|
class TestSqliteThreadSharing(TransactionTestCase):
|
||||||
|
available_apps = ['backends']
|
||||||
|
|
||||||
|
def test_database_sharing_in_threads(self):
|
||||||
|
def create_object():
|
||||||
|
models.Object.objects.create()
|
||||||
|
|
||||||
|
create_object()
|
||||||
|
|
||||||
|
thread = threading.Thread(target=create_object)
|
||||||
|
thread.start()
|
||||||
|
thread.join()
|
||||||
|
|
||||||
|
self.assertEqual(models.Object.objects.count(), 2)
|
||||||
|
|
Loading…
Reference in New Issue