From 767dc47e1ac40271010c96a70b3368c7d93f25f9 Mon Sep 17 00:00:00 2001 From: Renan Ivo Date: Fri, 28 Oct 2016 21:58:44 -0200 Subject: [PATCH 1/8] Create "querycount" parameter --- pytest_django/plugin.py | 58 ++++++++++++++++++++++++++ tests/test_report.py | 90 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 148 insertions(+) create mode 100644 tests/test_report.py diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 5d4f323a7..f84e8ff16 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -86,6 +86,11 @@ def pytest_addoption(parser): 'Fail for invalid variables in templates.', type='bool', default=False) + group._addoption('--querycount', + action='store', type=int, default=None, metavar='N', + help='Show top N tests with most queries ' + '(N=0 for all).') + def _exists(path, ignore=EnvironmentError): try: @@ -337,6 +342,59 @@ def pytest_runtest_setup(item): _disable_class_methods(cls) +@pytest.hookimpl(hookwrapper=True) +def pytest_runtest_call(item): + count_parameter = item.config.option.querycount + if count_parameter is None: + yield + return + + from django.test.utils import CaptureQueriesContext + from django.db import connection + + with CaptureQueriesContext(connection) as context: + yield + + item.add_report_section('call', 'queries', context.captured_queries) + + +def pytest_terminal_summary(terminalreporter, exitstatus): + count_parameter = terminalreporter.config.option.querycount + if count_parameter is None: + return + + if count_parameter: + header = 'top {} tests with most queries'.format(count_parameter) + reports_slice = slice(None, count_parameter) + else: + header = 'top tests with most queries' + reports_slice = slice(None, None) + + terminalreporter.write_sep('=', header) + + def get_query_count(report): + sections = dict(report.sections) + return len(sections.get('Captured queries call', [])) + + reports = ( + terminalreporter.stats.get('failed', []) + + terminalreporter.stats.get('passed', []) + ) + + reports.sort(key=get_query_count) + reports.reverse() + + for report in reports[reports_slice]: + count = get_query_count(report) + nodeid = report.nodeid.replace("::()::", "::") + + terminalreporter.write_line('{count: <4} {when: <8} {nodeid}'.format( + count=count, + when=report.when, + nodeid=nodeid + )) + + @pytest.fixture(autouse=True, scope='session') def django_test_environment(request): """ diff --git a/tests/test_report.py b/tests/test_report.py new file mode 100644 index 000000000..53d853a73 --- /dev/null +++ b/tests/test_report.py @@ -0,0 +1,90 @@ +class TestQueryCount(object): + """Test report generated by --querycount parameter""" + + def test_querycount_report_header(self, django_testdir): + django_testdir.create_test_module(''' + def test_zero_queries(): + pass + ''') + + result = django_testdir.runpytest_subprocess('--querycount=5') + result.stdout.fnmatch_lines([ + '*== top 5 tests with most queries ==*' + ]) + + def test_header_not_set_without_parameter(self, django_testdir): + django_testdir.create_test_module(''' + def test_zero_queries(): + pass + ''') + + result = django_testdir.runpytest_subprocess() + assert 'tests with most queries' not in result.stdout.str() + + def test_querycount_report_lines(self, django_testdir): + django_testdir.create_test_module(''' + import pytest + from django.db import connection + + @pytest.mark.django_db + def test_one_query(): + with connection.cursor() as cursor: + cursor.execute('SELECT 1') + + assert True + + @pytest.mark.django_db + def test_two_queries(): + with connection.cursor() as cursor: + cursor.execute('SELECT 1') + cursor.execute('SELECT 1') + + assert True + + @pytest.mark.django_db + def test_failed_one_query(): + with connection.cursor() as cursor: + cursor.execute('SELECT 1') + + assert False + + def test_zero_queries(): + assert True + ''') + + result = django_testdir.runpytest_subprocess('--querycount=3') + lines = result.stdout.get_lines_after( + '*top 3 tests with most queries*' + ) + assert 'test_two_queries' in lines[0] + assert 'test_one_query' in lines[1] + assert 'test_failed' in lines[2] + assert 'test_zero_queries' not in result.stdout.str() + + def test_report_all_lines_on_querycount_zero(self, django_testdir): + django_testdir.create_test_module(''' + import pytest + from django.db import connection + + @pytest.mark.django_db + def test_one_query(): + with connection.cursor() as cursor: + cursor.execute('SELECT 1') + + assert True + + @pytest.mark.django_db + def test_two_queries(): + with connection.cursor() as cursor: + cursor.execute('SELECT 1') + cursor.execute('SELECT 1') + + assert True + ''') + + result = django_testdir.runpytest_subprocess('--querycount=0') + lines = result.stdout.get_lines_after( + '*top tests with most queries*' + ) + assert 'test_two_queries' in lines[0] + assert 'test_one_query' in lines[1] From cd37548914452dddd04d828448a12626361ec68a Mon Sep 17 00:00:00 2001 From: Renan Ivo Date: Fri, 28 Oct 2016 22:04:19 -0200 Subject: [PATCH 2/8] Add documenation for "--querycount" parameter --- docs/usage.rst | 6 ++++++ tests/test_report.py | 8 ++++---- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index 4c357e0ea..067d07e0a 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -29,6 +29,12 @@ Additional command line options ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Fail tests that render templates which make use of invalid template variables. + +``--querycount`` - show top N tests with most queries +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Show a list of top N tests which executed most queries. Use `--querycount=0` +to display a list of all tests ordered by the number of queries executed. + Running tests in parallel with pytest-xdist ------------------------------------------- pytest-django supports running tests on multiple processes to speed up test diff --git a/tests/test_report.py b/tests/test_report.py index 53d853a73..01497a989 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -19,7 +19,7 @@ def test_zero_queries(): ''') result = django_testdir.runpytest_subprocess() - assert 'tests with most queries' not in result.stdout.str() + assert 'top tests with most queries' not in result.stdout.str() def test_querycount_report_lines(self, django_testdir): django_testdir.create_test_module(''' @@ -52,14 +52,14 @@ def test_zero_queries(): assert True ''') - result = django_testdir.runpytest_subprocess('--querycount=3') + result = django_testdir.runpytest_subprocess('--querycount=4') lines = result.stdout.get_lines_after( - '*top 3 tests with most queries*' + '*top 4 tests with most queries*' ) assert 'test_two_queries' in lines[0] assert 'test_one_query' in lines[1] assert 'test_failed' in lines[2] - assert 'test_zero_queries' not in result.stdout.str() + assert 'test_zero_queries' in lines[3] def test_report_all_lines_on_querycount_zero(self, django_testdir): django_testdir.create_test_module(''' From 060c9d50d3cbf0016334e72fd7e767e61edc69ea Mon Sep 17 00:00:00 2001 From: Renan Ivo Date: Sun, 30 Oct 2016 12:51:22 -0200 Subject: [PATCH 3/8] Remove exitstatus from pytest_terminal_summary --- pytest_django/plugin.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index f84e8ff16..a89925ac7 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -358,7 +358,7 @@ def pytest_runtest_call(item): item.add_report_section('call', 'queries', context.captured_queries) -def pytest_terminal_summary(terminalreporter, exitstatus): +def pytest_terminal_summary(terminalreporter): count_parameter = terminalreporter.config.option.querycount if count_parameter is None: return From d45c6b32b3bdf837be226151739be07c101ff2d7 Mon Sep 17 00:00:00 2001 From: Renan Ivo Date: Fri, 30 Dec 2016 08:50:23 -0200 Subject: [PATCH 4/8] Display the number of queries executed by the fixtures when --querycount is used with --setup-show --- pytest_django/plugin.py | 26 ++++++++++++++++++++++++++ tests/test_report.py | 22 ++++++++++++++++++++++ 2 files changed, 48 insertions(+) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index a89925ac7..0eab70d6a 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -342,6 +342,32 @@ def pytest_runtest_setup(item): _disable_class_methods(cls) +@pytest.hookimpl(hookwrapper=True) +def pytest_fixture_setup(fixturedef, request): + config = request.config + + if config.option.querycount is None or not config.option.setupshow: + yield + return + + from django.test.utils import CaptureQueriesContext + from django.db import connection + + with CaptureQueriesContext(connection) as context: + yield + + querycount = len(context.captured_queries) + + if querycount: + capman = config.pluginmanager.getplugin('capturemanager') + capman.suspendcapture() + + tw = config.get_terminal_writer() + tw.write(' (# queries executed: {})'.format(querycount)) + + capman.resumecapture() + + @pytest.hookimpl(hookwrapper=True) def pytest_runtest_call(item): count_parameter = item.config.option.querycount diff --git a/tests/test_report.py b/tests/test_report.py index 01497a989..4316f7d98 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -88,3 +88,25 @@ def test_two_queries(): ) assert 'test_two_queries' in lines[0] assert 'test_one_query' in lines[1] + + def test_should_report_fixture_queries(self, django_testdir): + django_testdir.create_test_module(''' + import pytest + from django.db import connection + + @pytest.fixture + def one_query(): + with connection.cursor() as cursor: + cursor.execute('SELECT 1') + + @pytest.mark.django_db + def test_without_queries(one_query): + pass + ''') + + result = django_testdir.runpytest_subprocess( + '--setup-show', + '--querycount=5' + ) + + assert '(# queries executed: 1)' in result.stdout.str() From 564c04bbe1be9333e6900b00104162b517898b78 Mon Sep 17 00:00:00 2001 From: Renan Ivo Date: Fri, 30 Dec 2016 21:07:10 -0200 Subject: [PATCH 5/8] Add the usage of --setup-show in conjunction with --querycount to the docs --- docs/usage.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/usage.rst b/docs/usage.rst index 067d07e0a..161caacd8 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -35,6 +35,10 @@ Fail tests that render templates which make use of invalid template variables. Show a list of top N tests which executed most queries. Use `--querycount=0` to display a list of all tests ordered by the number of queries executed. +Using it in conjunction with `--setup-show` will display the number of +queries executed by each fixture (when the number of queries executed by the +fixture is greater than zero). + Running tests in parallel with pytest-xdist ------------------------------------------- pytest-django supports running tests on multiple processes to speed up test From fd9e6fb62bfd70649ed608332fdaf26ef2abefcd Mon Sep 17 00:00:00 2001 From: Renan Ivo Date: Sun, 17 Dec 2017 20:48:30 -0200 Subject: [PATCH 6/8] Fix report of the number of queries executed by fixtures --- pytest_django/plugin.py | 25 ++++++++++++++++--------- tests/test_report.py | 2 +- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 0eab70d6a..40054f51d 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -353,19 +353,26 @@ def pytest_fixture_setup(fixturedef, request): from django.test.utils import CaptureQueriesContext from django.db import connection - with CaptureQueriesContext(connection) as context: - yield + _blocking_manager.unblock() - querycount = len(context.captured_queries) + try: + with CaptureQueriesContext(connection) as context: + yield + except Exception: + yield + else: + querycount = len(context.captured_queries) - if querycount: - capman = config.pluginmanager.getplugin('capturemanager') - capman.suspendcapture() + if querycount: + capman = config.pluginmanager.getplugin('capturemanager') + capman.suspend_global_capture() - tw = config.get_terminal_writer() - tw.write(' (# queries executed: {})'.format(querycount)) + tw = config.get_terminal_writer() + tw.write(' (# of queries executed: {})'.format(querycount)) - capman.resumecapture() + capman.resume_global_capture() + finally: + _blocking_manager.restore() @pytest.hookimpl(hookwrapper=True) diff --git a/tests/test_report.py b/tests/test_report.py index 4316f7d98..c47cee0f9 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -109,4 +109,4 @@ def test_without_queries(one_query): '--querycount=5' ) - assert '(# queries executed: 1)' in result.stdout.str() + assert '(# of queries executed: 1)' in result.stdout.str() From e646ae9b1d84402c49946213675fe0461b463760 Mon Sep 17 00:00:00 2001 From: Renan Ivo Date: Mon, 22 Jan 2018 23:53:09 -0200 Subject: [PATCH 7/8] Add --noquerycount parameter to disable --querycount --- docs/usage.rst | 2 +- pytest_django/plugin.py | 7 ++++++- tests/test_report.py | 13 ++++++++++++- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/docs/usage.rst b/docs/usage.rst index 161caacd8..dd37b5cb4 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -37,7 +37,7 @@ to display a list of all tests ordered by the number of queries executed. Using it in conjunction with `--setup-show` will display the number of queries executed by each fixture (when the number of queries executed by the -fixture is greater than zero). +fixture is greater than zero). Use `--noquerycount` to force the disable of it. Running tests in parallel with pytest-xdist ------------------------------------------- diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 40054f51d..dfe4b1c2e 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -87,9 +87,14 @@ def pytest_addoption(parser): type='bool', default=False) group._addoption('--querycount', - action='store', type=int, default=None, metavar='N', + action='store', dest='querycount', type=int, + default=None, metavar='N', help='Show top N tests with most queries ' '(N=0 for all).') + group._addoption('--noquerycount', '--no-querycount', + action='store_const', dest='querycount', + const=None, default=None, + help='Disable --querycount, when both are used.') def _exists(path, ignore=EnvironmentError): diff --git a/tests/test_report.py b/tests/test_report.py index c47cee0f9..fb9b2e7a6 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -19,7 +19,18 @@ def test_zero_queries(): ''') result = django_testdir.runpytest_subprocess() - assert 'top tests with most queries' not in result.stdout.str() + assert 'tests with most queries' not in result.stdout.str() + + def test_disabled_when_noquerycount_is_also_used(self, django_testdir): + django_testdir.create_test_module(''' + def test_zero_queries(): + pass + ''') + + result = django_testdir.runpytest_subprocess( + '--querycount=5 --noquerycount' + ) + assert 'tests with most queries' not in result.stdout.str() def test_querycount_report_lines(self, django_testdir): django_testdir.create_test_module(''' From ffc35ea399740d1bffbbd64df4004d4f677f64b2 Mon Sep 17 00:00:00 2001 From: Renan Ivo Date: Tue, 23 Jan 2018 23:47:45 -0200 Subject: [PATCH 8/8] Add link to query optimization docs at the end of the --querycount report --- pytest_django/plugin.py | 12 ++++++++++++ tests/test_report.py | 24 ++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index dfe4b1c2e..86278821e 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -432,6 +432,18 @@ def get_query_count(report): nodeid=nodeid )) + import django + major, minor = django.VERSION[0:2] + + terminalreporter.write_line('') + terminalreporter.write_line( + '-- Docs: https://docs.djangoproject.com' + '/en/{major}.{minor}/topics/db/optimization/'.format( + major=major, + minor=minor + ) + ) + @pytest.fixture(autouse=True, scope='session') def django_test_environment(request): diff --git a/tests/test_report.py b/tests/test_report.py index fb9b2e7a6..22a1fd51f 100644 --- a/tests/test_report.py +++ b/tests/test_report.py @@ -32,6 +32,30 @@ def test_zero_queries(): ) assert 'tests with most queries' not in result.stdout.str() + def test_query_optimization_tips_for_the_current_version_of_django( + self, + django_testdir + ): + django_testdir.create_test_module(''' + def test_zero_queries(): + pass + ''') + + result = django_testdir.runpytest_subprocess('--querycount=5') + + import django + major, minor = django.VERSION[0:2] + + url = ( + 'https://docs.djangoproject.com' + '/en/{major}.{minor}/topics/db/optimization/' + ).format( + major=major, + minor=minor + ) + + assert url in result.stdout.str() + def test_querycount_report_lines(self, django_testdir): django_testdir.create_test_module(''' import pytest