|
29
|
1 |
import sys |
|
|
2 |
import signal |
|
0
|
3 |
import unittest |
|
29
|
4 |
|
|
0
|
5 |
from django.conf import settings |
|
|
6 |
from django.db.models import get_app, get_apps |
|
|
7 |
from django.test import _doctest as doctest |
|
|
8 |
from django.test.utils import setup_test_environment, teardown_test_environment |
|
|
9 |
from django.test.testcases import OutputChecker, DocTestRunner, TestCase |
|
|
10 |
|
|
|
11 |
# The module name for tests outside models.py |
|
|
12 |
TEST_MODULE = 'tests' |
|
|
13 |
|
|
|
14 |
doctestOutputChecker = OutputChecker() |
|
|
15 |
|
|
29
|
16 |
class DjangoTestRunner(unittest.TextTestRunner): |
|
|
17 |
|
|
|
18 |
def __init__(self, verbosity=0, failfast=False, **kwargs): |
|
|
19 |
super(DjangoTestRunner, self).__init__(verbosity=verbosity, **kwargs) |
|
|
20 |
self.failfast = failfast |
|
|
21 |
self._keyboard_interrupt_intercepted = False |
|
|
22 |
|
|
|
23 |
def run(self, *args, **kwargs): |
|
|
24 |
""" |
|
|
25 |
Runs the test suite after registering a custom signal handler |
|
|
26 |
that triggers a graceful exit when Ctrl-C is pressed. |
|
|
27 |
""" |
|
|
28 |
self._default_keyboard_interrupt_handler = signal.signal(signal.SIGINT, |
|
|
29 |
self._keyboard_interrupt_handler) |
|
|
30 |
try: |
|
|
31 |
result = super(DjangoTestRunner, self).run(*args, **kwargs) |
|
|
32 |
finally: |
|
|
33 |
signal.signal(signal.SIGINT, self._default_keyboard_interrupt_handler) |
|
|
34 |
return result |
|
|
35 |
|
|
|
36 |
def _keyboard_interrupt_handler(self, signal_number, stack_frame): |
|
|
37 |
""" |
|
|
38 |
Handles Ctrl-C by setting a flag that will stop the test run when |
|
|
39 |
the currently running test completes. |
|
|
40 |
""" |
|
|
41 |
self._keyboard_interrupt_intercepted = True |
|
|
42 |
sys.stderr.write(" <Test run halted by Ctrl-C> ") |
|
|
43 |
# Set the interrupt handler back to the default handler, so that |
|
|
44 |
# another Ctrl-C press will trigger immediate exit. |
|
|
45 |
signal.signal(signal.SIGINT, self._default_keyboard_interrupt_handler) |
|
|
46 |
|
|
|
47 |
def _makeResult(self): |
|
|
48 |
result = super(DjangoTestRunner, self)._makeResult() |
|
|
49 |
failfast = self.failfast |
|
|
50 |
|
|
|
51 |
def stoptest_override(func): |
|
|
52 |
def stoptest(test): |
|
|
53 |
# If we were set to failfast and the unit test failed, |
|
|
54 |
# or if the user has typed Ctrl-C, report and quit |
|
|
55 |
if (failfast and not result.wasSuccessful()) or \ |
|
|
56 |
self._keyboard_interrupt_intercepted: |
|
|
57 |
result.stop() |
|
|
58 |
func(test) |
|
|
59 |
return stoptest |
|
|
60 |
|
|
|
61 |
setattr(result, 'stopTest', stoptest_override(result.stopTest)) |
|
|
62 |
return result |
|
|
63 |
|
|
0
|
64 |
def get_tests(app_module): |
|
|
65 |
try: |
|
|
66 |
app_path = app_module.__name__.split('.')[:-1] |
|
|
67 |
test_module = __import__('.'.join(app_path + [TEST_MODULE]), {}, {}, TEST_MODULE) |
|
|
68 |
except ImportError, e: |
|
|
69 |
# Couldn't import tests.py. Was it due to a missing file, or |
|
|
70 |
# due to an import error in a tests.py that actually exists? |
|
|
71 |
import os.path |
|
|
72 |
from imp import find_module |
|
|
73 |
try: |
|
|
74 |
mod = find_module(TEST_MODULE, [os.path.dirname(app_module.__file__)]) |
|
|
75 |
except ImportError: |
|
|
76 |
# 'tests' module doesn't exist. Move on. |
|
|
77 |
test_module = None |
|
|
78 |
else: |
|
|
79 |
# The module exists, so there must be an import error in the |
|
|
80 |
# test module itself. We don't need the module; so if the |
|
|
81 |
# module was a single file module (i.e., tests.py), close the file |
|
|
82 |
# handle returned by find_module. Otherwise, the test module |
|
|
83 |
# is a directory, and there is nothing to close. |
|
|
84 |
if mod[0]: |
|
|
85 |
mod[0].close() |
|
|
86 |
raise |
|
|
87 |
return test_module |
|
|
88 |
|
|
|
89 |
def build_suite(app_module): |
|
|
90 |
"Create a complete Django test suite for the provided application module" |
|
|
91 |
suite = unittest.TestSuite() |
|
|
92 |
|
|
|
93 |
# Load unit and doctests in the models.py module. If module has |
|
|
94 |
# a suite() method, use it. Otherwise build the test suite ourselves. |
|
|
95 |
if hasattr(app_module, 'suite'): |
|
|
96 |
suite.addTest(app_module.suite()) |
|
|
97 |
else: |
|
|
98 |
suite.addTest(unittest.defaultTestLoader.loadTestsFromModule(app_module)) |
|
|
99 |
try: |
|
|
100 |
suite.addTest(doctest.DocTestSuite(app_module, |
|
|
101 |
checker=doctestOutputChecker, |
|
|
102 |
runner=DocTestRunner)) |
|
|
103 |
except ValueError: |
|
|
104 |
# No doc tests in models.py |
|
|
105 |
pass |
|
|
106 |
|
|
|
107 |
# Check to see if a separate 'tests' module exists parallel to the |
|
|
108 |
# models module |
|
|
109 |
test_module = get_tests(app_module) |
|
|
110 |
if test_module: |
|
|
111 |
# Load unit and doctests in the tests.py module. If module has |
|
|
112 |
# a suite() method, use it. Otherwise build the test suite ourselves. |
|
|
113 |
if hasattr(test_module, 'suite'): |
|
|
114 |
suite.addTest(test_module.suite()) |
|
|
115 |
else: |
|
|
116 |
suite.addTest(unittest.defaultTestLoader.loadTestsFromModule(test_module)) |
|
|
117 |
try: |
|
|
118 |
suite.addTest(doctest.DocTestSuite(test_module, |
|
|
119 |
checker=doctestOutputChecker, |
|
|
120 |
runner=DocTestRunner)) |
|
|
121 |
except ValueError: |
|
|
122 |
# No doc tests in tests.py |
|
|
123 |
pass |
|
|
124 |
return suite |
|
|
125 |
|
|
|
126 |
def build_test(label): |
|
29
|
127 |
"""Construct a test case with the specified label. Label should be of the |
|
|
128 |
form model.TestClass or model.TestClass.test_method. Returns an |
|
|
129 |
instantiated test or test suite corresponding to the label provided. |
|
0
|
130 |
|
|
|
131 |
""" |
|
|
132 |
parts = label.split('.') |
|
|
133 |
if len(parts) < 2 or len(parts) > 3: |
|
|
134 |
raise ValueError("Test label '%s' should be of the form app.TestCase or app.TestCase.test_method" % label) |
|
|
135 |
|
|
29
|
136 |
# |
|
|
137 |
# First, look for TestCase instances with a name that matches |
|
|
138 |
# |
|
0
|
139 |
app_module = get_app(parts[0]) |
|
29
|
140 |
test_module = get_tests(app_module) |
|
0
|
141 |
TestClass = getattr(app_module, parts[1], None) |
|
|
142 |
|
|
|
143 |
# Couldn't find the test class in models.py; look in tests.py |
|
|
144 |
if TestClass is None: |
|
|
145 |
if test_module: |
|
|
146 |
TestClass = getattr(test_module, parts[1], None) |
|
|
147 |
|
|
29
|
148 |
try: |
|
|
149 |
if issubclass(TestClass, unittest.TestCase): |
|
|
150 |
if len(parts) == 2: # label is app.TestClass |
|
|
151 |
try: |
|
|
152 |
return unittest.TestLoader().loadTestsFromTestCase(TestClass) |
|
|
153 |
except TypeError: |
|
|
154 |
raise ValueError("Test label '%s' does not refer to a test class" % label) |
|
|
155 |
else: # label is app.TestClass.test_method |
|
|
156 |
return TestClass(parts[2]) |
|
|
157 |
except TypeError: |
|
|
158 |
# TestClass isn't a TestClass - it must be a method or normal class |
|
|
159 |
pass |
|
|
160 |
|
|
|
161 |
# |
|
|
162 |
# If there isn't a TestCase, look for a doctest that matches |
|
|
163 |
# |
|
|
164 |
tests = [] |
|
|
165 |
for module in app_module, test_module: |
|
0
|
166 |
try: |
|
29
|
167 |
doctests = doctest.DocTestSuite(module, |
|
|
168 |
checker=doctestOutputChecker, |
|
|
169 |
runner=DocTestRunner) |
|
|
170 |
# Now iterate over the suite, looking for doctests whose name |
|
|
171 |
# matches the pattern that was given |
|
|
172 |
for test in doctests: |
|
|
173 |
if test._dt_test.name in ( |
|
|
174 |
'%s.%s' % (module.__name__, '.'.join(parts[1:])), |
|
|
175 |
'%s.__test__.%s' % (module.__name__, '.'.join(parts[1:]))): |
|
|
176 |
tests.append(test) |
|
|
177 |
except ValueError: |
|
|
178 |
# No doctests found. |
|
|
179 |
pass |
|
0
|
180 |
|
|
29
|
181 |
# If no tests were found, then we were given a bad test label. |
|
|
182 |
if not tests: |
|
|
183 |
raise ValueError("Test label '%s' does not refer to a test" % label) |
|
|
184 |
|
|
|
185 |
# Construct a suite out of the tests that matched. |
|
|
186 |
return unittest.TestSuite(tests) |
|
0
|
187 |
|
|
|
188 |
def partition_suite(suite, classes, bins): |
|
|
189 |
""" |
|
|
190 |
Partitions a test suite by test type. |
|
|
191 |
|
|
|
192 |
classes is a sequence of types |
|
|
193 |
bins is a sequence of TestSuites, one more than classes |
|
|
194 |
|
|
|
195 |
Tests of type classes[i] are added to bins[i], |
|
|
196 |
tests with no match found in classes are place in bins[-1] |
|
|
197 |
""" |
|
|
198 |
for test in suite: |
|
|
199 |
if isinstance(test, unittest.TestSuite): |
|
|
200 |
partition_suite(test, classes, bins) |
|
|
201 |
else: |
|
|
202 |
for i in range(len(classes)): |
|
|
203 |
if isinstance(test, classes[i]): |
|
|
204 |
bins[i].addTest(test) |
|
|
205 |
break |
|
|
206 |
else: |
|
|
207 |
bins[-1].addTest(test) |
|
|
208 |
|
|
|
209 |
def reorder_suite(suite, classes): |
|
|
210 |
""" |
|
|
211 |
Reorders a test suite by test type. |
|
|
212 |
|
|
|
213 |
classes is a sequence of types |
|
|
214 |
|
|
|
215 |
All tests of type clases[0] are placed first, then tests of type classes[1], etc. |
|
|
216 |
Tests with no match in classes are placed last. |
|
|
217 |
""" |
|
|
218 |
class_count = len(classes) |
|
|
219 |
bins = [unittest.TestSuite() for i in range(class_count+1)] |
|
|
220 |
partition_suite(suite, classes, bins) |
|
|
221 |
for i in range(class_count): |
|
|
222 |
bins[0].addTests(bins[i+1]) |
|
|
223 |
return bins[0] |
|
|
224 |
|
|
29
|
225 |
|
|
|
226 |
class DjangoTestSuiteRunner(object): |
|
|
227 |
def __init__(self, verbosity=1, interactive=True, failfast=True, **kwargs): |
|
|
228 |
self.verbosity = verbosity |
|
|
229 |
self.interactive = interactive |
|
|
230 |
self.failfast = failfast |
|
|
231 |
|
|
|
232 |
def setup_test_environment(self, **kwargs): |
|
|
233 |
setup_test_environment() |
|
|
234 |
settings.DEBUG = False |
|
|
235 |
|
|
|
236 |
def build_suite(self, test_labels, extra_tests=None, **kwargs): |
|
|
237 |
suite = unittest.TestSuite() |
|
|
238 |
|
|
|
239 |
if test_labels: |
|
|
240 |
for label in test_labels: |
|
|
241 |
if '.' in label: |
|
|
242 |
suite.addTest(build_test(label)) |
|
|
243 |
else: |
|
|
244 |
app = get_app(label) |
|
|
245 |
suite.addTest(build_suite(app)) |
|
|
246 |
else: |
|
|
247 |
for app in get_apps(): |
|
|
248 |
suite.addTest(build_suite(app)) |
|
0
|
249 |
|
|
29
|
250 |
if extra_tests: |
|
|
251 |
for test in extra_tests: |
|
|
252 |
suite.addTest(test) |
|
0
|
253 |
|
|
29
|
254 |
return reorder_suite(suite, (TestCase,)) |
|
0
|
255 |
|
|
29
|
256 |
def setup_databases(self, **kwargs): |
|
|
257 |
from django.db import connections |
|
|
258 |
old_names = [] |
|
|
259 |
mirrors = [] |
|
|
260 |
for alias in connections: |
|
|
261 |
connection = connections[alias] |
|
|
262 |
# If the database is a test mirror, redirect it's connection |
|
|
263 |
# instead of creating a test database. |
|
|
264 |
if connection.settings_dict['TEST_MIRROR']: |
|
|
265 |
mirrors.append((alias, connection)) |
|
|
266 |
mirror_alias = connection.settings_dict['TEST_MIRROR'] |
|
|
267 |
connections._connections[alias] = connections[mirror_alias] |
|
|
268 |
else: |
|
|
269 |
old_names.append((connection, connection.settings_dict['NAME'])) |
|
|
270 |
connection.creation.create_test_db(self.verbosity, autoclobber=not self.interactive) |
|
|
271 |
return old_names, mirrors |
|
0
|
272 |
|
|
29
|
273 |
def run_suite(self, suite, **kwargs): |
|
|
274 |
return DjangoTestRunner(verbosity=self.verbosity, failfast=self.failfast).run(suite) |
|
0
|
275 |
|
|
29
|
276 |
def teardown_databases(self, old_config, **kwargs): |
|
|
277 |
from django.db import connections |
|
|
278 |
old_names, mirrors = old_config |
|
|
279 |
# Point all the mirrors back to the originals |
|
|
280 |
for alias, connection in mirrors: |
|
|
281 |
connections._connections[alias] = connection |
|
|
282 |
# Destroy all the non-mirror databases |
|
|
283 |
for connection, old_name in old_names: |
|
|
284 |
connection.creation.destroy_test_db(old_name, self.verbosity) |
|
|
285 |
|
|
|
286 |
def teardown_test_environment(self, **kwargs): |
|
|
287 |
teardown_test_environment() |
|
|
288 |
|
|
|
289 |
def suite_result(self, suite, result, **kwargs): |
|
|
290 |
return len(result.failures) + len(result.errors) |
|
0
|
291 |
|
|
29
|
292 |
def run_tests(self, test_labels, extra_tests=None, **kwargs): |
|
|
293 |
""" |
|
|
294 |
Run the unit tests for all the test labels in the provided list. |
|
|
295 |
Labels must be of the form: |
|
|
296 |
- app.TestClass.test_method |
|
|
297 |
Run a single specific test method |
|
|
298 |
- app.TestClass |
|
|
299 |
Run all the test methods in a given class |
|
|
300 |
- app |
|
|
301 |
Search for doctests and unittests in the named application. |
|
0
|
302 |
|
|
29
|
303 |
When looking for tests, the test runner will look in the models and |
|
|
304 |
tests modules for the application. |
|
|
305 |
|
|
|
306 |
A list of 'extra' tests may also be provided; these tests |
|
|
307 |
will be added to the test suite. |
|
0
|
308 |
|
|
29
|
309 |
Returns the number of tests that failed. |
|
|
310 |
""" |
|
|
311 |
self.setup_test_environment() |
|
|
312 |
suite = self.build_suite(test_labels, extra_tests) |
|
|
313 |
old_config = self.setup_databases() |
|
|
314 |
result = self.run_suite(suite) |
|
|
315 |
self.teardown_databases(old_config) |
|
|
316 |
self.teardown_test_environment() |
|
|
317 |
return self.suite_result(suite, result) |
|
0
|
318 |
|
|
29
|
319 |
def run_tests(test_labels, verbosity=1, interactive=True, failfast=False, extra_tests=None): |
|
|
320 |
import warnings |
|
|
321 |
warnings.warn( |
|
|
322 |
'The run_tests() test runner has been deprecated in favor of DjangoTestSuiteRunner.', |
|
|
323 |
PendingDeprecationWarning |
|
|
324 |
) |
|
|
325 |
test_runner = DjangoTestSuiteRunner(verbosity=verbosity, interactive=interactive, failfast=failfast) |
|
|
326 |
return test_runner.run_tests(test_labels, extra_tests=extra_tests) |