|
1 import sys |
|
2 import signal |
|
3 import unittest |
|
4 |
|
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 |
|
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 |
|
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): |
|
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. |
|
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 |
|
136 # |
|
137 # First, look for TestCase instances with a name that matches |
|
138 # |
|
139 app_module = get_app(parts[0]) |
|
140 test_module = get_tests(app_module) |
|
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 |
|
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: |
|
166 try: |
|
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 |
|
180 |
|
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) |
|
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 |
|
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)) |
|
249 |
|
250 if extra_tests: |
|
251 for test in extra_tests: |
|
252 suite.addTest(test) |
|
253 |
|
254 return reorder_suite(suite, (TestCase,)) |
|
255 |
|
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 |
|
272 |
|
273 def run_suite(self, suite, **kwargs): |
|
274 return DjangoTestRunner(verbosity=self.verbosity, failfast=self.failfast).run(suite) |
|
275 |
|
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) |
|
291 |
|
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. |
|
302 |
|
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. |
|
308 |
|
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) |
|
318 |
|
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) |