|
1 #!/usr/bin/env python |
|
2 |
|
3 import os, subprocess, sys, traceback |
|
4 import unittest |
|
5 |
|
6 import django.contrib as contrib |
|
7 |
|
8 CONTRIB_DIR_NAME = 'django.contrib' |
|
9 MODEL_TESTS_DIR_NAME = 'modeltests' |
|
10 REGRESSION_TESTS_DIR_NAME = 'regressiontests' |
|
11 |
|
12 TEST_TEMPLATE_DIR = 'templates' |
|
13 |
|
14 CONTRIB_DIR = os.path.dirname(contrib.__file__) |
|
15 MODEL_TEST_DIR = os.path.join(os.path.dirname(__file__), MODEL_TESTS_DIR_NAME) |
|
16 REGRESSION_TEST_DIR = os.path.join(os.path.dirname(__file__), REGRESSION_TESTS_DIR_NAME) |
|
17 |
|
18 REGRESSION_SUBDIRS_TO_SKIP = ['locale'] |
|
19 |
|
20 ALWAYS_INSTALLED_APPS = [ |
|
21 'django.contrib.contenttypes', |
|
22 'django.contrib.auth', |
|
23 'django.contrib.sites', |
|
24 'django.contrib.flatpages', |
|
25 'django.contrib.redirects', |
|
26 'django.contrib.sessions', |
|
27 'django.contrib.messages', |
|
28 'django.contrib.comments', |
|
29 'django.contrib.admin', |
|
30 'django.contrib.admindocs', |
|
31 ] |
|
32 |
|
33 def geodjango(settings): |
|
34 # All databases must have spatial backends to run GeoDjango tests. |
|
35 spatial_dbs = [name for name, db_dict in settings.DATABASES.items() |
|
36 if db_dict['ENGINE'].startswith('django.contrib.gis')] |
|
37 return len(spatial_dbs) == len(settings.DATABASES) |
|
38 |
|
39 def get_test_models(): |
|
40 models = [] |
|
41 for loc, dirpath in (MODEL_TESTS_DIR_NAME, MODEL_TEST_DIR), (REGRESSION_TESTS_DIR_NAME, REGRESSION_TEST_DIR), (CONTRIB_DIR_NAME, CONTRIB_DIR): |
|
42 for f in os.listdir(dirpath): |
|
43 if f.startswith('__init__') or f.startswith('.') or \ |
|
44 f.startswith('sql') or f.startswith('invalid') or \ |
|
45 os.path.basename(f) in REGRESSION_SUBDIRS_TO_SKIP: |
|
46 continue |
|
47 models.append((loc, f)) |
|
48 return models |
|
49 |
|
50 def get_invalid_models(): |
|
51 models = [] |
|
52 for loc, dirpath in (MODEL_TESTS_DIR_NAME, MODEL_TEST_DIR), (REGRESSION_TESTS_DIR_NAME, REGRESSION_TEST_DIR), (CONTRIB_DIR_NAME, CONTRIB_DIR): |
|
53 for f in os.listdir(dirpath): |
|
54 if f.startswith('__init__') or f.startswith('.') or f.startswith('sql'): |
|
55 continue |
|
56 if f.startswith('invalid'): |
|
57 models.append((loc, f)) |
|
58 return models |
|
59 |
|
60 class InvalidModelTestCase(unittest.TestCase): |
|
61 def __init__(self, model_label): |
|
62 unittest.TestCase.__init__(self) |
|
63 self.model_label = model_label |
|
64 |
|
65 def runTest(self): |
|
66 from django.core.management.validation import get_validation_errors |
|
67 from django.db.models.loading import load_app |
|
68 from cStringIO import StringIO |
|
69 |
|
70 try: |
|
71 module = load_app(self.model_label) |
|
72 except Exception, e: |
|
73 self.fail('Unable to load invalid model module') |
|
74 |
|
75 # Make sure sys.stdout is not a tty so that we get errors without |
|
76 # coloring attached (makes matching the results easier). We restore |
|
77 # sys.stderr afterwards. |
|
78 orig_stdout = sys.stdout |
|
79 s = StringIO() |
|
80 sys.stdout = s |
|
81 count = get_validation_errors(s, module) |
|
82 sys.stdout = orig_stdout |
|
83 s.seek(0) |
|
84 error_log = s.read() |
|
85 actual = error_log.split('\n') |
|
86 expected = module.model_errors.split('\n') |
|
87 |
|
88 unexpected = [err for err in actual if err not in expected] |
|
89 missing = [err for err in expected if err not in actual] |
|
90 |
|
91 self.assert_(not unexpected, "Unexpected Errors: " + '\n'.join(unexpected)) |
|
92 self.assert_(not missing, "Missing Errors: " + '\n'.join(missing)) |
|
93 |
|
94 def setup(verbosity, test_labels): |
|
95 from django.conf import settings |
|
96 state = { |
|
97 'INSTALLED_APPS': settings.INSTALLED_APPS, |
|
98 'ROOT_URLCONF': getattr(settings, "ROOT_URLCONF", ""), |
|
99 'TEMPLATE_DIRS': settings.TEMPLATE_DIRS, |
|
100 'USE_I18N': settings.USE_I18N, |
|
101 'LOGIN_URL': settings.LOGIN_URL, |
|
102 'LANGUAGE_CODE': settings.LANGUAGE_CODE, |
|
103 'MIDDLEWARE_CLASSES': settings.MIDDLEWARE_CLASSES, |
|
104 } |
|
105 |
|
106 # Redirect some settings for the duration of these tests. |
|
107 settings.INSTALLED_APPS = ALWAYS_INSTALLED_APPS |
|
108 settings.ROOT_URLCONF = 'urls' |
|
109 settings.TEMPLATE_DIRS = (os.path.join(os.path.dirname(__file__), TEST_TEMPLATE_DIR),) |
|
110 settings.USE_I18N = True |
|
111 settings.LANGUAGE_CODE = 'en' |
|
112 settings.LOGIN_URL = '/accounts/login/' |
|
113 settings.MIDDLEWARE_CLASSES = ( |
|
114 'django.contrib.sessions.middleware.SessionMiddleware', |
|
115 'django.contrib.auth.middleware.AuthenticationMiddleware', |
|
116 'django.contrib.messages.middleware.MessageMiddleware', |
|
117 'django.middleware.common.CommonMiddleware', |
|
118 ) |
|
119 settings.SITE_ID = 1 |
|
120 # For testing comment-utils, we require the MANAGERS attribute |
|
121 # to be set, so that a test email is sent out which we catch |
|
122 # in our tests. |
|
123 settings.MANAGERS = ("admin@djangoproject.com",) |
|
124 |
|
125 # Load all the ALWAYS_INSTALLED_APPS. |
|
126 # (This import statement is intentionally delayed until after we |
|
127 # access settings because of the USE_I18N dependency.) |
|
128 from django.db.models.loading import get_apps, load_app |
|
129 get_apps() |
|
130 |
|
131 # Load all the test model apps. |
|
132 test_labels_set = set([label.split('.')[0] for label in test_labels]) |
|
133 test_models = get_test_models() |
|
134 |
|
135 # If GeoDjango, then we'll want to add in the test applications |
|
136 # that are a part of its test suite. |
|
137 if geodjango(settings): |
|
138 from django.contrib.gis.tests import geo_apps |
|
139 test_models.extend(geo_apps(runtests=True)) |
|
140 |
|
141 for model_dir, model_name in test_models: |
|
142 model_label = '.'.join([model_dir, model_name]) |
|
143 # if the model was named on the command line, or |
|
144 # no models were named (i.e., run all), import |
|
145 # this model and add it to the list to test. |
|
146 if not test_labels or model_name in test_labels_set: |
|
147 if verbosity >= 1: |
|
148 print "Importing model %s" % model_name |
|
149 mod = load_app(model_label) |
|
150 if mod: |
|
151 if model_label not in settings.INSTALLED_APPS: |
|
152 settings.INSTALLED_APPS.append(model_label) |
|
153 |
|
154 return state |
|
155 |
|
156 def teardown(state): |
|
157 from django.conf import settings |
|
158 # Restore the old settings. |
|
159 for key, value in state.items(): |
|
160 setattr(settings, key, value) |
|
161 |
|
162 def django_tests(verbosity, interactive, failfast, test_labels): |
|
163 from django.conf import settings |
|
164 state = setup(verbosity, test_labels) |
|
165 |
|
166 # Add tests for invalid models. |
|
167 extra_tests = [] |
|
168 for model_dir, model_name in get_invalid_models(): |
|
169 model_label = '.'.join([model_dir, model_name]) |
|
170 if not test_labels or model_name in test_labels: |
|
171 extra_tests.append(InvalidModelTestCase(model_label)) |
|
172 try: |
|
173 # Invalid models are not working apps, so we cannot pass them into |
|
174 # the test runner with the other test_labels |
|
175 test_labels.remove(model_name) |
|
176 except ValueError: |
|
177 pass |
|
178 |
|
179 # If GeoDjango is used, add it's tests that aren't a part of |
|
180 # an application (e.g., GEOS, GDAL, Distance objects). |
|
181 if geodjango(settings): |
|
182 from django.contrib.gis.tests import geodjango_suite |
|
183 extra_tests.append(geodjango_suite(apps=False)) |
|
184 |
|
185 # Run the test suite, including the extra validation tests. |
|
186 from django.test.utils import get_runner |
|
187 if not hasattr(settings, 'TEST_RUNNER'): |
|
188 settings.TEST_RUNNER = 'django.test.simple.DjangoTestSuiteRunner' |
|
189 TestRunner = get_runner(settings) |
|
190 |
|
191 if hasattr(TestRunner, 'func_name'): |
|
192 # Pre 1.2 test runners were just functions, |
|
193 # and did not support the 'failfast' option. |
|
194 import warnings |
|
195 warnings.warn( |
|
196 'Function-based test runners are deprecated. Test runners should be classes with a run_tests() method.', |
|
197 PendingDeprecationWarning |
|
198 ) |
|
199 failures = TestRunner(test_labels, verbosity=verbosity, interactive=interactive, |
|
200 extra_tests=extra_tests) |
|
201 else: |
|
202 test_runner = TestRunner(verbosity=verbosity, interactive=interactive, failfast=failfast) |
|
203 failures = test_runner.run_tests(test_labels, extra_tests=extra_tests) |
|
204 |
|
205 teardown(state) |
|
206 return failures |
|
207 |
|
208 |
|
209 def bisect_tests(bisection_label, options, test_labels): |
|
210 state = setup(int(options.verbosity), test_labels) |
|
211 |
|
212 if not test_labels: |
|
213 # Get the full list of test labels to use for bisection |
|
214 from django.db.models.loading import get_apps |
|
215 test_labels = [app.__name__.split('.')[-2] for app in get_apps()] |
|
216 |
|
217 print '***** Bisecting test suite:',' '.join(test_labels) |
|
218 |
|
219 # Make sure the bisection point isn't in the test list |
|
220 # Also remove tests that need to be run in specific combinations |
|
221 for label in [bisection_label, 'model_inheritance_same_model_name']: |
|
222 try: |
|
223 test_labels.remove(label) |
|
224 except ValueError: |
|
225 pass |
|
226 |
|
227 subprocess_args = ['python','runtests.py', '--settings=%s' % options.settings] |
|
228 if options.failfast: |
|
229 subprocess_args.append('--failfast') |
|
230 if options.verbosity: |
|
231 subprocess_args.append('--verbosity=%s' % options.verbosity) |
|
232 if not options.interactive: |
|
233 subprocess_args.append('--noinput') |
|
234 |
|
235 iteration = 1 |
|
236 while len(test_labels) > 1: |
|
237 midpoint = len(test_labels)/2 |
|
238 test_labels_a = test_labels[:midpoint] + [bisection_label] |
|
239 test_labels_b = test_labels[midpoint:] + [bisection_label] |
|
240 print '***** Pass %da: Running the first half of the test suite' % iteration |
|
241 print '***** Test labels:',' '.join(test_labels_a) |
|
242 failures_a = subprocess.call(subprocess_args + test_labels_a) |
|
243 |
|
244 print '***** Pass %db: Running the second half of the test suite' % iteration |
|
245 print '***** Test labels:',' '.join(test_labels_b) |
|
246 print |
|
247 failures_b = subprocess.call(subprocess_args + test_labels_b) |
|
248 |
|
249 if failures_a and not failures_b: |
|
250 print "***** Problem found in first half. Bisecting again..." |
|
251 iteration = iteration + 1 |
|
252 test_labels = test_labels_a[:-1] |
|
253 elif failures_b and not failures_a: |
|
254 print "***** Problem found in second half. Bisecting again..." |
|
255 iteration = iteration + 1 |
|
256 test_labels = test_labels_b[:-1] |
|
257 elif failures_a and failures_b: |
|
258 print "***** Multiple sources of failure found" |
|
259 break |
|
260 else: |
|
261 print "***** No source of failure found... try pair execution (--pair)" |
|
262 break |
|
263 |
|
264 if len(test_labels) == 1: |
|
265 print "***** Source of error:",test_labels[0] |
|
266 teardown(state) |
|
267 |
|
268 def paired_tests(paired_test, options, test_labels): |
|
269 state = setup(int(options.verbosity), test_labels) |
|
270 |
|
271 if not test_labels: |
|
272 print "" |
|
273 # Get the full list of test labels to use for bisection |
|
274 from django.db.models.loading import get_apps |
|
275 test_labels = [app.__name__.split('.')[-2] for app in get_apps()] |
|
276 |
|
277 print '***** Trying paired execution' |
|
278 |
|
279 # Make sure the bisection point isn't in the test list |
|
280 # Also remove tests that need to be run in specific combinations |
|
281 for label in [paired_test, 'model_inheritance_same_model_name']: |
|
282 try: |
|
283 test_labels.remove(label) |
|
284 except ValueError: |
|
285 pass |
|
286 |
|
287 subprocess_args = ['python','runtests.py', '--settings=%s' % options.settings] |
|
288 if options.failfast: |
|
289 subprocess_args.append('--failfast') |
|
290 if options.verbosity: |
|
291 subprocess_args.append('--verbosity=%s' % options.verbosity) |
|
292 if not options.interactive: |
|
293 subprocess_args.append('--noinput') |
|
294 |
|
295 for i, label in enumerate(test_labels): |
|
296 print '***** %d of %d: Check test pairing with %s' % (i+1, len(test_labels), label) |
|
297 failures = subprocess.call(subprocess_args + [label, paired_test]) |
|
298 if failures: |
|
299 print '***** Found problem pair with',label |
|
300 return |
|
301 |
|
302 print '***** No problem pair found' |
|
303 teardown(state) |
|
304 |
|
305 if __name__ == "__main__": |
|
306 from optparse import OptionParser |
|
307 usage = "%prog [options] [model model model ...]" |
|
308 parser = OptionParser(usage=usage) |
|
309 parser.add_option('-v','--verbosity', action='store', dest='verbosity', default='0', |
|
310 type='choice', choices=['0', '1', '2'], |
|
311 help='Verbosity level; 0=minimal output, 1=normal output, 2=all output') |
|
312 parser.add_option('--noinput', action='store_false', dest='interactive', default=True, |
|
313 help='Tells Django to NOT prompt the user for input of any kind.') |
|
314 parser.add_option('--failfast', action='store_true', dest='failfast', default=False, |
|
315 help='Tells Django to stop running the test suite after first failed test.') |
|
316 parser.add_option('--settings', |
|
317 help='Python path to settings module, e.g. "myproject.settings". If this isn\'t provided, the DJANGO_SETTINGS_MODULE environment variable will be used.') |
|
318 parser.add_option('--bisect', action='store', dest='bisect', default=None, |
|
319 help="Bisect the test suite to discover a test that causes a test failure when combined with the named test.") |
|
320 parser.add_option('--pair', action='store', dest='pair', default=None, |
|
321 help="Run the test suite in pairs with the named test to find problem pairs.") |
|
322 options, args = parser.parse_args() |
|
323 if options.settings: |
|
324 os.environ['DJANGO_SETTINGS_MODULE'] = options.settings |
|
325 elif "DJANGO_SETTINGS_MODULE" not in os.environ: |
|
326 parser.error("DJANGO_SETTINGS_MODULE is not set in the environment. " |
|
327 "Set it or use --settings.") |
|
328 |
|
329 if options.bisect: |
|
330 bisect_tests(options.bisect, options, args) |
|
331 elif options.pair: |
|
332 paired_tests(options.pair, options, args) |
|
333 else: |
|
334 failures = django_tests(int(options.verbosity), options.interactive, options.failfast, args) |
|
335 if failures: |
|
336 sys.exit(bool(failures)) |