mirror of https://github.com/django/django.git
Fixed #24118 -- Added --debug-sql option for tests.
Added a --debug-sql option for tests and runtests.py which outputs the SQL logger for failing tests. When combined with --verbosity=2, it also outputs the SQL for passing tests. Thanks to Berker, Tim, Markus, Shai, Josh and Anssi for review and discussion.
This commit is contained in:
parent
68a439a18d
commit
b5c1a85b50
|
@ -1,4 +1,5 @@
|
||||||
from importlib import import_module
|
from importlib import import_module
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import unittest
|
import unittest
|
||||||
from unittest import TestSuite, defaultTestLoader
|
from unittest import TestSuite, defaultTestLoader
|
||||||
|
@ -8,6 +9,47 @@ from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.test import SimpleTestCase, TestCase
|
from django.test import SimpleTestCase, TestCase
|
||||||
from django.test.utils import setup_test_environment, teardown_test_environment
|
from django.test.utils import setup_test_environment, teardown_test_environment
|
||||||
from django.utils.datastructures import OrderedSet
|
from django.utils.datastructures import OrderedSet
|
||||||
|
from django.utils.six import StringIO
|
||||||
|
|
||||||
|
|
||||||
|
class DebugSQLTextTestResult(unittest.TextTestResult):
|
||||||
|
def __init__(self, stream, descriptions, verbosity):
|
||||||
|
self.logger = logging.getLogger('django.db.backends')
|
||||||
|
self.logger.setLevel(logging.DEBUG)
|
||||||
|
super(DebugSQLTextTestResult, self).__init__(stream, descriptions, verbosity)
|
||||||
|
|
||||||
|
def startTest(self, test):
|
||||||
|
self.debug_sql_stream = StringIO()
|
||||||
|
self.handler = logging.StreamHandler(self.debug_sql_stream)
|
||||||
|
self.logger.addHandler(self.handler)
|
||||||
|
super(DebugSQLTextTestResult, self).startTest(test)
|
||||||
|
|
||||||
|
def stopTest(self, test):
|
||||||
|
super(DebugSQLTextTestResult, self).stopTest(test)
|
||||||
|
self.logger.removeHandler(self.handler)
|
||||||
|
if self.showAll:
|
||||||
|
self.debug_sql_stream.seek(0)
|
||||||
|
self.stream.write(self.debug_sql_stream.read())
|
||||||
|
self.stream.writeln(self.separator2)
|
||||||
|
|
||||||
|
def addError(self, test, err):
|
||||||
|
super(DebugSQLTextTestResult, self).addError(test, err)
|
||||||
|
self.debug_sql_stream.seek(0)
|
||||||
|
self.errors[-1] = self.errors[-1] + (self.debug_sql_stream.read(),)
|
||||||
|
|
||||||
|
def addFailure(self, test, err):
|
||||||
|
super(DebugSQLTextTestResult, self).addFailure(test, err)
|
||||||
|
self.debug_sql_stream.seek(0)
|
||||||
|
self.failures[-1] = self.failures[-1] + (self.debug_sql_stream.read(),)
|
||||||
|
|
||||||
|
def printErrorList(self, flavour, errors):
|
||||||
|
for test, err, sql_debug in errors:
|
||||||
|
self.stream.writeln(self.separator1)
|
||||||
|
self.stream.writeln("%s: %s" % (flavour, self.getDescription(test)))
|
||||||
|
self.stream.writeln(self.separator2)
|
||||||
|
self.stream.writeln("%s" % err)
|
||||||
|
self.stream.writeln(self.separator2)
|
||||||
|
self.stream.writeln("%s" % sql_debug)
|
||||||
|
|
||||||
|
|
||||||
class DiscoverRunner(object):
|
class DiscoverRunner(object):
|
||||||
|
@ -20,9 +62,9 @@ class DiscoverRunner(object):
|
||||||
test_loader = defaultTestLoader
|
test_loader = defaultTestLoader
|
||||||
reorder_by = (TestCase, SimpleTestCase)
|
reorder_by = (TestCase, SimpleTestCase)
|
||||||
|
|
||||||
def __init__(self, pattern=None, top_level=None,
|
def __init__(self, pattern=None, top_level=None, verbosity=1,
|
||||||
verbosity=1, interactive=True, failfast=False, keepdb=False, reverse=False,
|
interactive=True, failfast=False, keepdb=False,
|
||||||
**kwargs):
|
reverse=False, debug_sql=False, **kwargs):
|
||||||
|
|
||||||
self.pattern = pattern
|
self.pattern = pattern
|
||||||
self.top_level = top_level
|
self.top_level = top_level
|
||||||
|
@ -32,6 +74,7 @@ class DiscoverRunner(object):
|
||||||
self.failfast = failfast
|
self.failfast = failfast
|
||||||
self.keepdb = keepdb
|
self.keepdb = keepdb
|
||||||
self.reverse = reverse
|
self.reverse = reverse
|
||||||
|
self.debug_sql = debug_sql
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def add_arguments(cls, parser):
|
def add_arguments(cls, parser):
|
||||||
|
@ -47,6 +90,9 @@ class DiscoverRunner(object):
|
||||||
parser.add_argument('-r', '--reverse', action='store_true', dest='reverse',
|
parser.add_argument('-r', '--reverse', action='store_true', dest='reverse',
|
||||||
default=False,
|
default=False,
|
||||||
help='Reverses test cases order.')
|
help='Reverses test cases order.')
|
||||||
|
parser.add_argument('-d', '--debug-sql', action='store_true', dest='debug_sql',
|
||||||
|
default=False,
|
||||||
|
help='Prints logged SQL queries on failure.')
|
||||||
|
|
||||||
def setup_test_environment(self, **kwargs):
|
def setup_test_environment(self, **kwargs):
|
||||||
setup_test_environment()
|
setup_test_environment()
|
||||||
|
@ -115,12 +161,20 @@ class DiscoverRunner(object):
|
||||||
return reorder_suite(suite, self.reorder_by, self.reverse)
|
return reorder_suite(suite, self.reorder_by, self.reverse)
|
||||||
|
|
||||||
def setup_databases(self, **kwargs):
|
def setup_databases(self, **kwargs):
|
||||||
return setup_databases(self.verbosity, self.interactive, self.keepdb, **kwargs)
|
return setup_databases(
|
||||||
|
self.verbosity, self.interactive, self.keepdb, self.debug_sql,
|
||||||
|
**kwargs
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_resultclass(self):
|
||||||
|
return DebugSQLTextTestResult if self.debug_sql else None
|
||||||
|
|
||||||
def run_suite(self, suite, **kwargs):
|
def run_suite(self, suite, **kwargs):
|
||||||
|
resultclass = self.get_resultclass()
|
||||||
return self.test_runner(
|
return self.test_runner(
|
||||||
verbosity=self.verbosity,
|
verbosity=self.verbosity,
|
||||||
failfast=self.failfast,
|
failfast=self.failfast,
|
||||||
|
resultclass=resultclass,
|
||||||
).run(suite)
|
).run(suite)
|
||||||
|
|
||||||
def teardown_databases(self, old_config, **kwargs):
|
def teardown_databases(self, old_config, **kwargs):
|
||||||
|
@ -266,7 +320,7 @@ def partition_suite(suite, classes, bins, reverse=False):
|
||||||
bins[-1].add(test)
|
bins[-1].add(test)
|
||||||
|
|
||||||
|
|
||||||
def setup_databases(verbosity, interactive, keepdb=False, **kwargs):
|
def setup_databases(verbosity, interactive, keepdb=False, debug_sql=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,
|
||||||
|
@ -326,4 +380,7 @@ def setup_databases(verbosity, interactive, keepdb=False, **kwargs):
|
||||||
connections[alias].settings_dict['NAME'] = (
|
connections[alias].settings_dict['NAME'] = (
|
||||||
connections[mirror_alias].settings_dict['NAME'])
|
connections[mirror_alias].settings_dict['NAME'])
|
||||||
|
|
||||||
|
if debug_sql:
|
||||||
|
for alias in connections:
|
||||||
|
connections[alias].force_debug_cursor = True
|
||||||
return old_names, mirrors
|
return old_names, mirrors
|
||||||
|
|
|
@ -286,7 +286,7 @@ For example, suppose that the failing test that works on its own is
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ ./runtests.py --bisect basic.tests.ModelTest.test_eq
|
$ ./runtests.py --bisect basic.tests.ModelTest.test_eq
|
||||||
|
|
||||||
will try to determine a test that interferes with the given one. First, the
|
will try to determine a test that interferes with the given one. First, the
|
||||||
test is run with the first half of the test suite. If a failure occurs, the
|
test is run with the first half of the test suite. If a failure occurs, the
|
||||||
|
@ -302,7 +302,7 @@ failure. So:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ ./runtests.py --pair basic.tests.ModelTest.test_eq
|
$ ./runtests.py --pair basic.tests.ModelTest.test_eq
|
||||||
|
|
||||||
will pair ``test_eq`` with every test label.
|
will pair ``test_eq`` with every test label.
|
||||||
|
|
||||||
|
@ -313,7 +313,7 @@ the first one:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ ./runtests.py --pair basic.tests.ModelTest.test_eq queries transactions
|
$ ./runtests.py --pair basic.tests.ModelTest.test_eq queries transactions
|
||||||
|
|
||||||
You can also try running any set of tests in reverse using the ``--reverse``
|
You can also try running any set of tests in reverse using the ``--reverse``
|
||||||
option in order to verify that executing tests in a different order does not
|
option in order to verify that executing tests in a different order does not
|
||||||
|
@ -321,8 +321,16 @@ cause any trouble:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
$ ./runtests.py basic --reverse
|
$ ./runtests.py basic --reverse
|
||||||
|
|
||||||
|
If you wish to examine the SQL being run in failing tests, you can turn on
|
||||||
|
:ref:`SQL logging <django-db-logger>` using the ``--debug-sql`` option. If you
|
||||||
|
combine this with ``--verbosity=2``, all SQL queries will be output.
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ ./runtests.py basic --debug-sql
|
||||||
|
|
||||||
.. versionadded:: 1.8
|
.. versionadded:: 1.8
|
||||||
|
|
||||||
The ``--reverse`` option was added.
|
The ``--reverse`` and ``--debug-sql`` options were added.
|
||||||
|
|
|
@ -1449,6 +1449,14 @@ This may help in debugging tests that aren't properly isolated and have side
|
||||||
effects. :ref:`Grouping by test class <order-of-tests>` is preserved when using
|
effects. :ref:`Grouping by test class <order-of-tests>` is preserved when using
|
||||||
this option.
|
this option.
|
||||||
|
|
||||||
|
.. django-admin-option:: --debug-sql
|
||||||
|
|
||||||
|
.. versionadded:: 1.8
|
||||||
|
|
||||||
|
The ``--debug-sql`` option can be used to enable :ref:`SQL logging
|
||||||
|
<django-db-logger>` for failing tests. If :djadminopt:`--verbosity` is ``2``,
|
||||||
|
then queries in passing tests are also output.
|
||||||
|
|
||||||
testserver <fixture fixture ...>
|
testserver <fixture fixture ...>
|
||||||
--------------------------------
|
--------------------------------
|
||||||
|
|
||||||
|
|
|
@ -625,8 +625,9 @@ Tests
|
||||||
allows you to test that two JSON fragments are not equal.
|
allows you to test that two JSON fragments are not equal.
|
||||||
|
|
||||||
* Added options to the :djadmin:`test` command to preserve the test database
|
* Added options to the :djadmin:`test` command to preserve the test database
|
||||||
(:djadminopt:`--keepdb`) and to run the test cases in reverse order
|
(:djadminopt:`--keepdb`), to run the test cases in reverse order
|
||||||
(:djadminopt:`--reverse`).
|
(:djadminopt:`--reverse`), and to enable SQL logging for failing tests
|
||||||
|
(:djadminopt:`--debug-sql`).
|
||||||
|
|
||||||
* Added the :attr:`~django.test.Response.resolver_match` attribute to test
|
* Added the :attr:`~django.test.Response.resolver_match` attribute to test
|
||||||
client responses.
|
client responses.
|
||||||
|
|
|
@ -439,6 +439,8 @@ Messages to this logger have the following extra context:
|
||||||
* ``request``: The request object that generated the logging
|
* ``request``: The request object that generated the logging
|
||||||
message.
|
message.
|
||||||
|
|
||||||
|
.. _django-db-logger:
|
||||||
|
|
||||||
``django.db.backends``
|
``django.db.backends``
|
||||||
~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
|
|
|
@ -355,7 +355,7 @@ behavior. This class defines the ``run_tests()`` entry point, plus a
|
||||||
selection of other methods that are used to by ``run_tests()`` to set up,
|
selection of other methods that are used to by ``run_tests()`` to set up,
|
||||||
execute and tear down the test suite.
|
execute and tear down the test suite.
|
||||||
|
|
||||||
.. class:: DiscoverRunner(pattern='test*.py', top_level=None, verbosity=1, interactive=True, failfast=True, keepdb=False, reverse=False, **kwargs)
|
.. class:: DiscoverRunner(pattern='test*.py', top_level=None, verbosity=1, interactive=True, failfast=True, keepdb=False, reverse=False, debug_sql=False, **kwargs)
|
||||||
|
|
||||||
``DiscoverRunner`` will search for tests in any file matching ``pattern``.
|
``DiscoverRunner`` will search for tests in any file matching ``pattern``.
|
||||||
|
|
||||||
|
@ -386,6 +386,11 @@ execute and tear down the test suite.
|
||||||
and have side effects. :ref:`Grouping by test class <order-of-tests>` is
|
and have side effects. :ref:`Grouping by test class <order-of-tests>` is
|
||||||
preserved when using this option.
|
preserved when using this option.
|
||||||
|
|
||||||
|
If ``debug_sql`` is ``True``, failing test cases will output SQL queries
|
||||||
|
logged to the :ref:`django.db.backends logger <django-db-logger>` as well
|
||||||
|
as the traceback. If ``verbosity`` is ``2``, then queries in all tests are
|
||||||
|
output.
|
||||||
|
|
||||||
Django may, from time to time, extend the capabilities of the test runner
|
Django may, from time to time, extend the capabilities of the test runner
|
||||||
by adding new arguments. The ``**kwargs`` declaration allows for this
|
by adding new arguments. The ``**kwargs`` declaration allows for this
|
||||||
expansion. If you subclass ``DiscoverRunner`` or write your own test
|
expansion. If you subclass ``DiscoverRunner`` or write your own test
|
||||||
|
@ -402,7 +407,7 @@ execute and tear down the test suite.
|
||||||
subclassed test runner to add options to the list of command-line
|
subclassed test runner to add options to the list of command-line
|
||||||
options that the :djadmin:`test` command could use.
|
options that the :djadmin:`test` command could use.
|
||||||
|
|
||||||
The ``keepdb`` and the ``reverse`` arguments were added.
|
The ``keepdb``, ``reverse``, and ``debug_sql`` arguments were added.
|
||||||
|
|
||||||
Attributes
|
Attributes
|
||||||
~~~~~~~~~~
|
~~~~~~~~~~
|
||||||
|
|
|
@ -217,7 +217,7 @@ def teardown(state):
|
||||||
setattr(settings, key, value)
|
setattr(settings, key, value)
|
||||||
|
|
||||||
|
|
||||||
def django_tests(verbosity, interactive, failfast, keepdb, reverse, test_labels):
|
def django_tests(verbosity, interactive, failfast, keepdb, reverse, test_labels, debug_sql):
|
||||||
state = setup(verbosity, test_labels)
|
state = setup(verbosity, test_labels)
|
||||||
extra_tests = []
|
extra_tests = []
|
||||||
|
|
||||||
|
@ -232,6 +232,7 @@ def django_tests(verbosity, interactive, failfast, keepdb, reverse, test_labels)
|
||||||
failfast=failfast,
|
failfast=failfast,
|
||||||
keepdb=keepdb,
|
keepdb=keepdb,
|
||||||
reverse=reverse,
|
reverse=reverse,
|
||||||
|
debug_sql=debug_sql,
|
||||||
)
|
)
|
||||||
# Catch warnings thrown in test DB setup -- remove in Django 1.9
|
# Catch warnings thrown in test DB setup -- remove in Django 1.9
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
|
@ -386,6 +387,9 @@ if __name__ == "__main__":
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
'--selenium', action='store_true', dest='selenium', default=False,
|
'--selenium', action='store_true', dest='selenium', default=False,
|
||||||
help='Run the Selenium tests as well (if Selenium is installed)')
|
help='Run the Selenium tests as well (if Selenium is installed)')
|
||||||
|
parser.add_argument(
|
||||||
|
'--debug-sql', action='store_true', dest='debug_sql', default=False,
|
||||||
|
help='Turn on the SQL query logger within tests')
|
||||||
options = parser.parse_args()
|
options = parser.parse_args()
|
||||||
|
|
||||||
# mock is a required dependency
|
# mock is a required dependency
|
||||||
|
@ -421,6 +425,7 @@ if __name__ == "__main__":
|
||||||
else:
|
else:
|
||||||
failures = django_tests(options.verbosity, options.interactive,
|
failures = django_tests(options.verbosity, options.interactive,
|
||||||
options.failfast, options.keepdb,
|
options.failfast, options.keepdb,
|
||||||
options.reverse, options.modules)
|
options.reverse, options.modules,
|
||||||
|
options.debug_sql)
|
||||||
if failures:
|
if failures:
|
||||||
sys.exit(bool(failures))
|
sys.exit(bool(failures))
|
||||||
|
|
|
@ -0,0 +1,102 @@
|
||||||
|
import unittest
|
||||||
|
|
||||||
|
from django.db import connection
|
||||||
|
from django.test import TestCase
|
||||||
|
from django.test.runner import DiscoverRunner
|
||||||
|
from django.utils import six
|
||||||
|
|
||||||
|
from .models import Person
|
||||||
|
|
||||||
|
|
||||||
|
@unittest.skipUnless(connection.vendor == 'sqlite', 'Only run on sqlite so we can check output SQL.')
|
||||||
|
class TestDebugSQL(unittest.TestCase):
|
||||||
|
|
||||||
|
class PassingTest(TestCase):
|
||||||
|
def runTest(self):
|
||||||
|
Person.objects.filter(first_name='pass').count()
|
||||||
|
|
||||||
|
class FailingTest(TestCase):
|
||||||
|
def runTest(self):
|
||||||
|
Person.objects.filter(first_name='fail').count()
|
||||||
|
self.fail()
|
||||||
|
|
||||||
|
class ErrorTest(TestCase):
|
||||||
|
def runTest(self):
|
||||||
|
Person.objects.filter(first_name='error').count()
|
||||||
|
raise Exception
|
||||||
|
|
||||||
|
def _test_output(self, verbosity):
|
||||||
|
runner = DiscoverRunner(debug_sql=True, verbosity=0)
|
||||||
|
suite = runner.test_suite()
|
||||||
|
suite.addTest(self.FailingTest())
|
||||||
|
suite.addTest(self.ErrorTest())
|
||||||
|
suite.addTest(self.PassingTest())
|
||||||
|
old_config = runner.setup_databases()
|
||||||
|
stream = six.StringIO()
|
||||||
|
resultclass = runner.get_resultclass()
|
||||||
|
runner.test_runner(
|
||||||
|
verbosity=verbosity,
|
||||||
|
stream=stream,
|
||||||
|
resultclass=resultclass,
|
||||||
|
).run(suite)
|
||||||
|
runner.teardown_databases(old_config)
|
||||||
|
|
||||||
|
stream.seek(0)
|
||||||
|
return stream.read()
|
||||||
|
|
||||||
|
def test_output_normal(self):
|
||||||
|
full_output = self._test_output(1)
|
||||||
|
for output in self.expected_outputs:
|
||||||
|
self.assertIn(output, full_output)
|
||||||
|
for output in self.verbose_expected_outputs:
|
||||||
|
self.assertNotIn(output, full_output)
|
||||||
|
|
||||||
|
def test_output_verbose(self):
|
||||||
|
full_output = self._test_output(2)
|
||||||
|
for output in self.expected_outputs:
|
||||||
|
self.assertIn(output, full_output)
|
||||||
|
for output in self.verbose_expected_outputs:
|
||||||
|
self.assertIn(output, full_output)
|
||||||
|
|
||||||
|
if six.PY3:
|
||||||
|
expected_outputs = [
|
||||||
|
('''QUERY = 'SELECT COUNT(%s) AS "__count" '''
|
||||||
|
'''FROM "test_runner_person" WHERE '''
|
||||||
|
'''"test_runner_person"."first_name" = %s' '''
|
||||||
|
'''- PARAMS = ('*', 'error');'''),
|
||||||
|
('''QUERY = 'SELECT COUNT(%s) AS "__count" '''
|
||||||
|
'''FROM "test_runner_person" WHERE '''
|
||||||
|
'''"test_runner_person"."first_name" = %s' '''
|
||||||
|
'''- PARAMS = ('*', 'fail');'''),
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
expected_outputs = [
|
||||||
|
('''QUERY = u'SELECT COUNT(%s) AS "__count" '''
|
||||||
|
'''FROM "test_runner_person" WHERE '''
|
||||||
|
'''"test_runner_person"."first_name" = %s' '''
|
||||||
|
'''- PARAMS = (u'*', u'error');'''),
|
||||||
|
('''QUERY = u'SELECT COUNT(%s) AS "__count" '''
|
||||||
|
'''FROM "test_runner_person" WHERE '''
|
||||||
|
'''"test_runner_person"."first_name" = %s' '''
|
||||||
|
'''- PARAMS = (u'*', u'fail');'''),
|
||||||
|
]
|
||||||
|
|
||||||
|
verbose_expected_outputs = [
|
||||||
|
'runTest (test_runner.test_debug_sql.FailingTest) ... FAIL',
|
||||||
|
'runTest (test_runner.test_debug_sql.ErrorTest) ... ERROR',
|
||||||
|
'runTest (test_runner.test_debug_sql.PassingTest) ... ok',
|
||||||
|
]
|
||||||
|
if six.PY3:
|
||||||
|
verbose_expected_outputs += [
|
||||||
|
('''QUERY = 'SELECT COUNT(%s) AS "__count" '''
|
||||||
|
'''FROM "test_runner_person" WHERE '''
|
||||||
|
'''"test_runner_person"."first_name" = %s' '''
|
||||||
|
'''- PARAMS = ('*', 'pass');'''),
|
||||||
|
]
|
||||||
|
else:
|
||||||
|
verbose_expected_outputs += [
|
||||||
|
('''QUERY = u'SELECT COUNT(%s) AS "__count" '''
|
||||||
|
'''FROM "test_runner_person" WHERE '''
|
||||||
|
'''"test_runner_person"."first_name" = %s' '''
|
||||||
|
'''- PARAMS = (u'*', u'pass');'''),
|
||||||
|
]
|
Loading…
Reference in New Issue