|
1 | 1 | # -*- coding: utf-8
|
| 2 | +import json |
| 3 | +import os |
| 4 | +import os.path |
| 5 | +import threading |
| 6 | + |
2 | 7 | from django.apps import AppConfig
|
3 | 8 | from django.conf import settings
|
4 |
| -from django.core.checks import Error, register |
5 |
| -from django.utils import inspect |
6 |
| -from django.utils.module_loading import import_string |
| 9 | +from django.test import TransactionTestCase |
| 10 | +from django.test.utils import get_runner |
| 11 | + |
| 12 | +from request_query_count.query_count import (TestCaseQueryContainer, |
| 13 | + TestResultQueryContainer) |
7 | 14 |
|
8 |
| -from request_query_count.query_count import Middleware |
| 15 | +local = threading.local() |
9 | 16 |
|
10 | 17 |
|
11 | 18 | class RequestQueryCountConfig(AppConfig):
|
| 19 | + LOCAL_TESTCASE_CONTAINER_NAME = 'querycount_test_case_container' |
| 20 | + LOCAL_RESULT_CONTAINER_NAME = 'querycount_result_container' |
| 21 | + |
12 | 22 | name = 'request_query_count'
|
13 | 23 | verbose_name = 'Request Query Count'
|
14 | 24 |
|
15 |
| - def ready(self): |
16 |
| - pass |
17 |
| - |
18 |
| - |
19 |
| -@register |
20 |
| -def check_middleware(_app_configs, **_kwargs): |
21 |
| - errors = [] |
22 |
| - |
23 |
| - setting = getattr(settings, 'MIDDLEWARE', None) |
24 |
| - setting_name = 'MIDDLEWARE' |
25 |
| - if setting is None: |
26 |
| - setting = settings.MIDDLEWARE_CLASSES |
27 |
| - setting_name = 'MIDDLEWARE_CLASSES' |
28 |
| - |
29 |
| - if not any(is_middleware_class(Middleware, middleware) |
30 |
| - for middleware in enumerate(setting)): |
31 |
| - errors.append( |
32 |
| - Error( |
33 |
| - "debug_toolbar.middleware.DebugToolbarMiddleware is missing " |
34 |
| - "from %s." % setting_name, |
35 |
| - hint="Add debug_toolbar.middleware.DebugToolbarMiddleware to " |
36 |
| - "%s." % setting_name, |
37 |
| - ) |
| 25 | + setting_name = 'REQUEST_QUERY_COUNT' |
| 26 | + |
| 27 | + default_settings = { |
| 28 | + 'ENABLE': True, |
| 29 | + 'ENABLE_STACKTRACES': True, |
| 30 | + 'DETAIL_PATH': 'reports/query_count_detail.json', |
| 31 | + 'SUMMARY_PATH': 'reports/query_count.json' |
| 32 | + } |
| 33 | + |
| 34 | + @classmethod |
| 35 | + def get_setting(cls, setting_name): |
| 36 | + return (getattr(settings, cls.setting_name, {}) |
| 37 | + .get(setting_name, cls.default_settings[setting_name])) |
| 38 | + |
| 39 | + @classmethod |
| 40 | + def stacktraces_enabled(cls): |
| 41 | + return cls.get_setting('ENABLE_STACKTRACES') |
| 42 | + |
| 43 | + @classmethod |
| 44 | + def enabled(cls): |
| 45 | + return cls.get_setting('ENABLE') |
| 46 | + |
| 47 | + @classmethod |
| 48 | + def get_testcase_container(cls): |
| 49 | + return getattr(local, cls.LOCAL_TESTCASE_CONTAINER_NAME) |
| 50 | + |
| 51 | + @classmethod |
| 52 | + def add_middleware(cls): |
| 53 | + setting = getattr(settings, 'MIDDLEWARE', None) |
| 54 | + setting_name = 'MIDDLEWARE' |
| 55 | + if setting is None: |
| 56 | + setting = settings.MIDDLEWARE_CLASSES |
| 57 | + setting_name = 'MIDDLEWARE_CLASSES' |
| 58 | + |
| 59 | + setattr( |
| 60 | + settings, |
| 61 | + setting_name, |
| 62 | + setting + ('request_query_count.middleware.Middleware',) |
38 | 63 | )
|
39 |
| - return errors |
40 |
| - |
41 |
| - |
42 |
| -def is_middleware_class(middleware_class, middleware_path): |
43 |
| - try: |
44 |
| - middleware_cls = import_string(middleware_path) |
45 |
| - except ImportError: |
46 |
| - return |
47 |
| - return ( |
48 |
| - inspect.isclass(middleware_cls) and |
49 |
| - issubclass(middleware_cls, middleware_class) |
50 |
| - ) |
| 64 | + |
| 65 | + @classmethod |
| 66 | + def wrap_set_up(cls, set_up): |
| 67 | + def wrapped(self, *args, **kwargs): |
| 68 | + result = set_up(self, *args, **kwargs) |
| 69 | + setattr(local, cls.LOCAL_TESTCASE_CONTAINER_NAME, |
| 70 | + TestCaseQueryContainer()) |
| 71 | + return result |
| 72 | + |
| 73 | + return wrapped |
| 74 | + |
| 75 | + @classmethod |
| 76 | + def wrap_tear_down(cls, tear_down): |
| 77 | + def wrapped(self, *args, **kwargs): |
| 78 | + if not hasattr(cls, 'test_result_container'): |
| 79 | + return tear_down(self, *args, *kwargs) |
| 80 | + |
| 81 | + container = cls.get_testcase_container() |
| 82 | + |
| 83 | + test_method = getattr(self, self._testMethodName) |
| 84 | + |
| 85 | + exclusions = ( |
| 86 | + getattr(self.__class__, "__querycount_exclude__", []) + |
| 87 | + getattr(test_method, "__querycount_exclude__", []) |
| 88 | + ) |
| 89 | + |
| 90 | + all_queries = cls.test_result_container |
| 91 | + current_queries = container.filter_by(exclusions) |
| 92 | + all_queries.add(self.id(), current_queries) |
| 93 | + |
| 94 | + return tear_down(self, *args, *kwargs) |
| 95 | + |
| 96 | + return wrapped |
| 97 | + |
| 98 | + @classmethod |
| 99 | + def patch_test_case(cls): |
| 100 | + TransactionTestCase.setUp = cls.wrap_set_up(TransactionTestCase.setUp) |
| 101 | + TransactionTestCase.tearDown = cls.wrap_tear_down( |
| 102 | + TransactionTestCase.tearDown) |
| 103 | + |
| 104 | + @classmethod |
| 105 | + def save_json(cls, setting_name, container, detail): |
| 106 | + summary_path = os.path.realpath(cls.get_setting(setting_name)) |
| 107 | + os.makedirs(os.path.dirname(summary_path), exist_ok=True) |
| 108 | + |
| 109 | + with open(summary_path, 'w') as json_file: |
| 110 | + json.dump(container.get_json(detail=detail), json_file, |
| 111 | + ensure_ascii=False, indent=4, sort_keys=True) |
| 112 | + |
| 113 | + @classmethod |
| 114 | + def wrap_testrunner_run(cls, func): |
| 115 | + def wrapped(self, *args, **kwargs): |
| 116 | + cls.test_result_container = TestResultQueryContainer() |
| 117 | + |
| 118 | + result = func(self, *args, **kwargs) |
| 119 | + |
| 120 | + cls.save_json('SUMMARY_PATH', cls.test_result_container, False) |
| 121 | + cls.save_json('DETAIL_PATH', cls.test_result_container, True) |
| 122 | + |
| 123 | + result.queries = cls.test_result_container |
| 124 | + del cls.test_result_container |
| 125 | + |
| 126 | + return result |
| 127 | + |
| 128 | + return wrapped |
| 129 | + |
| 130 | + @classmethod |
| 131 | + def patch_runner(cls): |
| 132 | + # FIXME: this is incompatible with --parallel and --test-runner |
| 133 | + # command arguments |
| 134 | + django_test_runner = get_runner(settings) |
| 135 | + test_runner = django_test_runner.test_runner |
| 136 | + test_runner.run = cls.wrap_testrunner_run(test_runner.run) |
| 137 | + |
| 138 | + def ready(self): |
| 139 | + if self.enabled(): |
| 140 | + self.add_middleware() |
| 141 | + self.patch_test_case() |
| 142 | + self.patch_runner() |
0 commit comments