1 | #! /usr/bin/env python2.3 |
---|
2 | ############################################################################## |
---|
3 | # |
---|
4 | # Copyright (c) 2001, 2002 Zope Corporation and Contributors. |
---|
5 | # All Rights Reserved. |
---|
6 | # |
---|
7 | # This software is subject to the provisions of the Zope Public License, |
---|
8 | # Version 2.0 (ZPL). A copy of the ZPL should accompany this distribution. |
---|
9 | # THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED |
---|
10 | # WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED |
---|
11 | # WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS |
---|
12 | # FOR A PARTICULAR PURPOSE. |
---|
13 | # |
---|
14 | ############################################################################## |
---|
15 | """ |
---|
16 | test.py [-abBcdDfFgGhklLmMPprstTuUv] [modfilter [testfilter]] |
---|
17 | |
---|
18 | Find and run tests written using the unittest module. |
---|
19 | |
---|
20 | The test runner searches for Python modules that contain test suites. |
---|
21 | It collects those suites, and runs the tests. There are many options |
---|
22 | for controlling how the tests are run. There are options for using |
---|
23 | the debugger, reporting code coverage, and checking for refcount problems. |
---|
24 | |
---|
25 | The test runner uses the following rules for finding tests to run. It |
---|
26 | searches for packages and modules that contain "tests" as a component |
---|
27 | of the name, e.g. "frob.tests.nitz" matches this rule because tests is |
---|
28 | a sub-package of frob. Within each "tests" package, it looks for |
---|
29 | modules that begin with the name "test." For each test module, it |
---|
30 | imports the module and calls the module's test_suite() function, which must |
---|
31 | return a unittest TestSuite object. |
---|
32 | |
---|
33 | Options can be specified as command line arguments (see below). However, |
---|
34 | options may also be specified in a file named 'test.config', a Python |
---|
35 | script which, if found, will be executed before the command line |
---|
36 | arguments are processed. |
---|
37 | |
---|
38 | The test.config script should specify options by setting zero or more of the |
---|
39 | global variables: LEVEL, BUILD, and other capitalized variable names found in |
---|
40 | the test runner script (see the list of global variables in process_args().). |
---|
41 | |
---|
42 | |
---|
43 | -a level |
---|
44 | --at-level level |
---|
45 | --all |
---|
46 | Run the tests at the given level. Any test at a level at or below |
---|
47 | this is run, any test at a level above this is not run. Level 0 |
---|
48 | runs all tests. The default is to run tests at level 1. --all is |
---|
49 | a shortcut for -a 0. |
---|
50 | |
---|
51 | -b |
---|
52 | --build |
---|
53 | Run "python setup.py build" before running tests, where "python" |
---|
54 | is the version of python used to run test.py. Highly recommended. |
---|
55 | Tests will be run from the build directory. |
---|
56 | |
---|
57 | -B |
---|
58 | --build-inplace |
---|
59 | Run "python setup.py build_ext -i" before running tests. Tests will be |
---|
60 | run from the source directory. |
---|
61 | |
---|
62 | -c |
---|
63 | --pychecker |
---|
64 | use pychecker |
---|
65 | |
---|
66 | -d |
---|
67 | --debug |
---|
68 | Instead of the normal test harness, run a debug version which |
---|
69 | doesn't catch any exceptions. This is occasionally handy when the |
---|
70 | unittest code catching the exception doesn't work right. |
---|
71 | Unfortunately, the debug harness doesn't print the name of the |
---|
72 | test, so Use With Care. |
---|
73 | |
---|
74 | -D |
---|
75 | --debug-inplace |
---|
76 | Works like -d, except that it loads pdb when an exception occurs. |
---|
77 | |
---|
78 | --dir directory |
---|
79 | -s directory |
---|
80 | Option to limit where tests are searched for. This is important |
---|
81 | when you *really* want to limit the code that gets run. This can |
---|
82 | be specified more than once to run tests in two different parts of |
---|
83 | the source tree. |
---|
84 | For example, if refactoring interfaces, you don't want to see the way |
---|
85 | you have broken setups for tests in other packages. You *just* want to |
---|
86 | run the interface tests. |
---|
87 | |
---|
88 | -f |
---|
89 | --skip-unit |
---|
90 | Run functional tests but not unit tests. |
---|
91 | Note that functional tests will be skipped if the module |
---|
92 | zope.app.tests.functional cannot be imported. |
---|
93 | Functional tests also expect to find the file ftesting.zcml, |
---|
94 | which is used to configure the functional-test run. |
---|
95 | |
---|
96 | -F |
---|
97 | DEPRECATED. Run both unit and functional tests. |
---|
98 | This option is deprecated, because this is the new default mode. |
---|
99 | Note that functional tests will be skipped if the module |
---|
100 | zope.app.tests.functional cannot be imported. |
---|
101 | |
---|
102 | -g threshold |
---|
103 | --gc-threshold threshold |
---|
104 | Set the garbage collector generation0 threshold. This can be used |
---|
105 | to stress memory and gc correctness. Some crashes are only |
---|
106 | reproducible when the threshold is set to 1 (agressive garbage |
---|
107 | collection). Do "-g 0" to disable garbage collection altogether. |
---|
108 | |
---|
109 | -G gc_option |
---|
110 | --gc-option gc_option |
---|
111 | Set the garbage collection debugging flags. The argument must be one |
---|
112 | of the DEBUG_ flags defined bythe Python gc module. Multiple options |
---|
113 | can be specified by using "-G OPTION1 -G OPTION2." |
---|
114 | |
---|
115 | -k |
---|
116 | --keepbytecode |
---|
117 | Do not delete all stale bytecode before running tests |
---|
118 | |
---|
119 | -l test_root |
---|
120 | --libdir test_root |
---|
121 | Search for tests starting in the specified start directory |
---|
122 | (useful for testing components being developed outside the main |
---|
123 | "src" or "build" trees). |
---|
124 | |
---|
125 | -L |
---|
126 | --loop |
---|
127 | Keep running the selected tests in a loop. You may experience |
---|
128 | memory leakage. |
---|
129 | |
---|
130 | -m |
---|
131 | -M minimal GUI. See -U. |
---|
132 | |
---|
133 | -P |
---|
134 | --profile |
---|
135 | Run the tests under hotshot and display the top 50 stats, sorted by |
---|
136 | cumulative time and number of calls. |
---|
137 | |
---|
138 | -p |
---|
139 | --progress |
---|
140 | Show running progress. It can be combined with -v or -vv. |
---|
141 | |
---|
142 | -r |
---|
143 | --refcount |
---|
144 | Look for refcount problems. |
---|
145 | This requires that Python was built --with-pydebug. |
---|
146 | |
---|
147 | -t |
---|
148 | --top-fifty |
---|
149 | Time the individual tests and print a list of the top 50, sorted from |
---|
150 | longest to shortest. |
---|
151 | |
---|
152 | --times n |
---|
153 | --times outfile |
---|
154 | With an integer argument, time the tests and print a list of the top <n> |
---|
155 | tests, sorted from longest to shortest. |
---|
156 | With a non-integer argument, specifies a file to which timing information |
---|
157 | is to be printed. |
---|
158 | |
---|
159 | -T |
---|
160 | --trace |
---|
161 | Use the trace module from Python for code coverage. The current |
---|
162 | utility writes coverage files to a directory named `coverage' that |
---|
163 | is parallel to `build'. It also prints a summary to stdout. |
---|
164 | |
---|
165 | -u |
---|
166 | --skip-functional |
---|
167 | CHANGED. Run unit tests but not functional tests. |
---|
168 | Note that the meaning of -u is changed from its former meaning, |
---|
169 | which is now specified by -U or --gui. |
---|
170 | |
---|
171 | -U |
---|
172 | --gui |
---|
173 | Use the PyUnit GUI instead of output to the command line. The GUI |
---|
174 | imports tests on its own, taking care to reload all dependencies |
---|
175 | on each run. The debug (-d), verbose (-v), progress (-p), and |
---|
176 | Loop (-L) options will be ignored. The testfilter filter is also |
---|
177 | not applied. |
---|
178 | |
---|
179 | -m |
---|
180 | -M |
---|
181 | --minimal-gui |
---|
182 | Note: -m is DEPRECATED in favour of -M or --minimal-gui. |
---|
183 | -m starts the gui minimized. Double-clicking the progress bar |
---|
184 | will start the import and run all tests. |
---|
185 | |
---|
186 | |
---|
187 | -v |
---|
188 | --verbose |
---|
189 | Verbose output. With one -v, unittest prints a dot (".") for each |
---|
190 | test run. With -vv, unittest prints the name of each test (for |
---|
191 | some definition of "name" ...). With no -v, unittest is silent |
---|
192 | until the end of the run, except when errors occur. |
---|
193 | |
---|
194 | When -p is also specified, the meaning of -v is slightly |
---|
195 | different. With -p and no -v only the percent indicator is |
---|
196 | displayed. With -p and -v the test name of the current test is |
---|
197 | shown to the right of the percent indicator. With -p and -vv the |
---|
198 | test name is not truncated to fit into 80 columns and it is not |
---|
199 | cleared after the test finishes. |
---|
200 | |
---|
201 | |
---|
202 | modfilter |
---|
203 | testfilter |
---|
204 | Case-sensitive regexps to limit which tests are run, used in search |
---|
205 | (not match) mode. |
---|
206 | In an extension of Python regexp notation, a leading "!" is stripped |
---|
207 | and causes the sense of the remaining regexp to be negated (so "!bc" |
---|
208 | matches any string that does not match "bc", and vice versa). |
---|
209 | By default these act like ".", i.e. nothing is excluded. |
---|
210 | |
---|
211 | modfilter is applied to a test file's path, starting at "build" and |
---|
212 | including (OS-dependent) path separators. |
---|
213 | |
---|
214 | testfilter is applied to the (method) name of the unittest methods |
---|
215 | contained in the test files whose paths modfilter matched. |
---|
216 | |
---|
217 | Extreme (yet useful) examples: |
---|
218 | |
---|
219 | test.py -vvb . "^testWriteClient$" |
---|
220 | |
---|
221 | Builds the project silently, then runs unittest in verbose mode on all |
---|
222 | tests whose names are precisely "testWriteClient". Useful when |
---|
223 | debugging a specific test. |
---|
224 | |
---|
225 | test.py -vvb . "!^testWriteClient$" |
---|
226 | |
---|
227 | As before, but runs all tests whose names aren't precisely |
---|
228 | "testWriteClient". Useful to avoid a specific failing test you don't |
---|
229 | want to deal with just yet. |
---|
230 | |
---|
231 | test.py -M . "!^testWriteClient$" |
---|
232 | |
---|
233 | As before, but now opens up a minimized PyUnit GUI window (only showing |
---|
234 | the progress bar). Useful for refactoring runs where you continually want |
---|
235 | to make sure all tests still pass. |
---|
236 | """ |
---|
237 | |
---|
238 | import gc |
---|
239 | import hotshot, hotshot.stats |
---|
240 | import os |
---|
241 | import re |
---|
242 | import pdb |
---|
243 | import sys |
---|
244 | import threading # just to get at Thread objects created by tests |
---|
245 | import time |
---|
246 | import traceback |
---|
247 | import unittest |
---|
248 | import warnings |
---|
249 | |
---|
250 | def set_trace_doctest(stdin=sys.stdin, stdout=sys.stdout, trace=pdb.set_trace): |
---|
251 | sys.stdin = stdin |
---|
252 | sys.stdout = stdout |
---|
253 | trace() |
---|
254 | |
---|
255 | pdb.set_trace_doctest = set_trace_doctest |
---|
256 | |
---|
257 | from distutils.util import get_platform |
---|
258 | |
---|
259 | PLAT_SPEC = "%s-%s" % (get_platform(), sys.version[0:3]) |
---|
260 | |
---|
261 | class ImmediateTestResult(unittest._TextTestResult): |
---|
262 | |
---|
263 | __super_init = unittest._TextTestResult.__init__ |
---|
264 | __super_startTest = unittest._TextTestResult.startTest |
---|
265 | __super_printErrors = unittest._TextTestResult.printErrors |
---|
266 | |
---|
267 | def __init__(self, stream, descriptions, verbosity, debug=False, |
---|
268 | count=None, progress=False): |
---|
269 | self.__super_init(stream, descriptions, verbosity) |
---|
270 | self._debug = debug |
---|
271 | self._progress = progress |
---|
272 | self._progressWithNames = False |
---|
273 | self.count = count |
---|
274 | self._testtimes = {} |
---|
275 | if progress and verbosity == 1: |
---|
276 | self.dots = False |
---|
277 | self._progressWithNames = True |
---|
278 | self._lastWidth = 0 |
---|
279 | self._maxWidth = 80 |
---|
280 | try: |
---|
281 | import curses |
---|
282 | except ImportError: |
---|
283 | pass |
---|
284 | else: |
---|
285 | curses.setupterm() |
---|
286 | self._maxWidth = curses.tigetnum('cols') |
---|
287 | self._maxWidth -= len("xxxx/xxxx (xxx.x%): ") + 1 |
---|
288 | |
---|
289 | def stopTest(self, test): |
---|
290 | self._testtimes[test] = time.time() - self._testtimes[test] |
---|
291 | if gc.garbage: |
---|
292 | print "The following test left garbage:" |
---|
293 | print test |
---|
294 | print gc.garbage |
---|
295 | # XXX Perhaps eat the garbage here, so that the garbage isn't |
---|
296 | # printed for every subsequent test. |
---|
297 | |
---|
298 | # Did the test leave any new threads behind? |
---|
299 | new_threads = [t for t in threading.enumerate() |
---|
300 | if (t.isAlive() |
---|
301 | and |
---|
302 | t not in self._threads)] |
---|
303 | if new_threads: |
---|
304 | print "The following test left new threads behind:" |
---|
305 | print test |
---|
306 | print "New thread(s):", new_threads |
---|
307 | |
---|
308 | def print_times(self, stream, count=None): |
---|
309 | results = self._testtimes.items() |
---|
310 | results.sort(lambda x, y: cmp(y[1], x[1])) |
---|
311 | if count: |
---|
312 | n = min(count, len(results)) |
---|
313 | if n: |
---|
314 | print >>stream, "Top %d longest tests:" % n |
---|
315 | else: |
---|
316 | n = len(results) |
---|
317 | if not n: |
---|
318 | return |
---|
319 | for i in range(n): |
---|
320 | print >>stream, "%6dms" % int(results[i][1] * 1000), results[i][0] |
---|
321 | |
---|
322 | def _print_traceback(self, msg, err, test, errlist): |
---|
323 | if self.showAll or self.dots or self._progress: |
---|
324 | self.stream.writeln("\n") |
---|
325 | self._lastWidth = 0 |
---|
326 | |
---|
327 | tb = "".join(traceback.format_exception(*err)) |
---|
328 | self.stream.writeln(msg) |
---|
329 | self.stream.writeln(tb) |
---|
330 | errlist.append((test, tb)) |
---|
331 | |
---|
332 | def startTest(self, test): |
---|
333 | if self._progress: |
---|
334 | self.stream.write("\r%4d" % (self.testsRun + 1)) |
---|
335 | if self.count: |
---|
336 | self.stream.write("/%d (%5.1f%%)" % (self.count, |
---|
337 | (self.testsRun + 1) * 100.0 / self.count)) |
---|
338 | if self.showAll: |
---|
339 | self.stream.write(": ") |
---|
340 | elif self._progressWithNames: |
---|
341 | # XXX will break with multibyte strings |
---|
342 | name = self.getShortDescription(test) |
---|
343 | width = len(name) |
---|
344 | if width < self._lastWidth: |
---|
345 | name += " " * (self._lastWidth - width) |
---|
346 | self.stream.write(": %s" % name) |
---|
347 | self._lastWidth = width |
---|
348 | self.stream.flush() |
---|
349 | self._threads = threading.enumerate() |
---|
350 | self.__super_startTest(test) |
---|
351 | self._testtimes[test] = time.time() |
---|
352 | |
---|
353 | def getShortDescription(self, test): |
---|
354 | s = self.getDescription(test) |
---|
355 | if len(s) > self._maxWidth: |
---|
356 | pos = s.find(" (") |
---|
357 | if pos >= 0: |
---|
358 | w = self._maxWidth - (pos + 5) |
---|
359 | if w < 1: |
---|
360 | # first portion (test method name) is too long |
---|
361 | s = s[:self._maxWidth-3] + "..." |
---|
362 | else: |
---|
363 | pre = s[:pos+2] |
---|
364 | post = s[-w:] |
---|
365 | s = "%s...%s" % (pre, post) |
---|
366 | return s[:self._maxWidth] |
---|
367 | |
---|
368 | def addError(self, test, err): |
---|
369 | if self._progress: |
---|
370 | self.stream.write("\r") |
---|
371 | if self._debug: |
---|
372 | raise err[0], err[1], err[2] |
---|
373 | self._print_traceback("Error in test %s" % test, err, |
---|
374 | test, self.errors) |
---|
375 | |
---|
376 | def addFailure(self, test, err): |
---|
377 | if self._progress: |
---|
378 | self.stream.write("\r") |
---|
379 | if self._debug: |
---|
380 | raise err[0], err[1], err[2] |
---|
381 | self._print_traceback("Failure in test %s" % test, err, |
---|
382 | test, self.failures) |
---|
383 | |
---|
384 | def printErrors(self): |
---|
385 | if self._progress and not (self.dots or self.showAll): |
---|
386 | self.stream.writeln() |
---|
387 | self.__super_printErrors() |
---|
388 | |
---|
389 | def printErrorList(self, flavor, errors): |
---|
390 | for test, err in errors: |
---|
391 | self.stream.writeln(self.separator1) |
---|
392 | self.stream.writeln("%s: %s" % (flavor, self.getDescription(test))) |
---|
393 | self.stream.writeln(self.separator2) |
---|
394 | self.stream.writeln(err) |
---|
395 | |
---|
396 | |
---|
397 | class ImmediateTestRunner(unittest.TextTestRunner): |
---|
398 | |
---|
399 | __super_init = unittest.TextTestRunner.__init__ |
---|
400 | |
---|
401 | def __init__(self, **kwarg): |
---|
402 | debug = kwarg.get("debug") |
---|
403 | if debug is not None: |
---|
404 | del kwarg["debug"] |
---|
405 | progress = kwarg.get("progress") |
---|
406 | if progress is not None: |
---|
407 | del kwarg["progress"] |
---|
408 | profile = kwarg.get("profile") |
---|
409 | if profile is not None: |
---|
410 | del kwarg["profile"] |
---|
411 | self.__super_init(**kwarg) |
---|
412 | self._debug = debug |
---|
413 | self._progress = progress |
---|
414 | self._profile = profile |
---|
415 | # Create the test result here, so that we can add errors if |
---|
416 | # the test suite search process has problems. The count |
---|
417 | # attribute must be set in run(), because we won't know the |
---|
418 | # count until all test suites have been found. |
---|
419 | self.result = ImmediateTestResult( |
---|
420 | self.stream, self.descriptions, self.verbosity, debug=self._debug, |
---|
421 | progress=self._progress) |
---|
422 | |
---|
423 | def _makeResult(self): |
---|
424 | # Needed base class run method. |
---|
425 | return self.result |
---|
426 | |
---|
427 | def run(self, test): |
---|
428 | self.result.count = test.countTestCases() |
---|
429 | if self._debug: |
---|
430 | club_debug(test) |
---|
431 | if self._profile: |
---|
432 | prof = hotshot.Profile("tests_profile.prof") |
---|
433 | args = (self, test) |
---|
434 | r = prof.runcall(unittest.TextTestRunner.run, *args) |
---|
435 | prof.close() |
---|
436 | stats = hotshot.stats.load("tests_profile.prof") |
---|
437 | stats.sort_stats('cumulative', 'calls') |
---|
438 | stats.print_stats(50) |
---|
439 | return r |
---|
440 | return unittest.TextTestRunner.run(self, test) |
---|
441 | |
---|
442 | def club_debug(test): |
---|
443 | # Beat a debug flag into debug-aware test cases |
---|
444 | setDebugModeOn = getattr(test, 'setDebugModeOn', None) |
---|
445 | if setDebugModeOn is not None: |
---|
446 | setDebugModeOn() |
---|
447 | |
---|
448 | for subtest in getattr(test, '_tests', ()): |
---|
449 | club_debug(subtest) |
---|
450 | |
---|
451 | # setup list of directories to put on the path |
---|
452 | class PathInit: |
---|
453 | def __init__(self, build, build_inplace, libdir=None): |
---|
454 | self.inplace = None |
---|
455 | # Figure out if we should test in-place or test in-build. If the -b |
---|
456 | # or -B option was given, test in the place we were told to build in. |
---|
457 | # Otherwise, we'll look for a build directory and if we find one, |
---|
458 | # we'll test there, otherwise we'll test in-place. |
---|
459 | if build: |
---|
460 | self.inplace = build_inplace |
---|
461 | if self.inplace is None: |
---|
462 | # Need to figure it out |
---|
463 | if os.path.isdir(os.path.join("build", "lib.%s" % PLAT_SPEC)): |
---|
464 | self.inplace = False |
---|
465 | else: |
---|
466 | self.inplace = True |
---|
467 | # Calculate which directories we're going to add to sys.path, and cd |
---|
468 | # to the appropriate working directory |
---|
469 | self.org_cwd = os.getcwd() |
---|
470 | if self.inplace: |
---|
471 | self.libdir = "src" |
---|
472 | else: |
---|
473 | self.libdir = "lib.%s" % PLAT_SPEC |
---|
474 | os.chdir("build") |
---|
475 | # Hack sys.path |
---|
476 | self.cwd = os.getcwd() |
---|
477 | sys.path.insert(0, os.path.join(self.cwd, self.libdir)) |
---|
478 | # Hack again for external products. |
---|
479 | global functional |
---|
480 | kind = functional and "FUNCTIONAL" or "UNIT" |
---|
481 | if libdir: |
---|
482 | extra = os.path.join(self.org_cwd, libdir) |
---|
483 | print "Running %s tests from %s" % (kind, extra) |
---|
484 | self.libdir = extra |
---|
485 | sys.path.insert(0, extra) |
---|
486 | else: |
---|
487 | print "Running %s tests from %s" % (kind, self.cwd) |
---|
488 | # Make sure functional tests find ftesting.zcml |
---|
489 | if functional: |
---|
490 | config_file = 'ftesting.zcml' |
---|
491 | if not self.inplace: |
---|
492 | # We chdired into build, so ftesting.zcml is in the |
---|
493 | # parent directory |
---|
494 | config_file = os.path.join('..', 'ftesting.zcml') |
---|
495 | print "Parsing %s" % config_file |
---|
496 | from zope.app.tests.functional import FunctionalTestSetup |
---|
497 | FunctionalTestSetup(config_file) |
---|
498 | |
---|
499 | def match(rx, s): |
---|
500 | if not rx: |
---|
501 | return True |
---|
502 | if rx[0] == "!": |
---|
503 | return re.search(rx[1:], s) is None |
---|
504 | else: |
---|
505 | return re.search(rx, s) is not None |
---|
506 | |
---|
507 | class TestFileFinder: |
---|
508 | def __init__(self, prefix): |
---|
509 | self.files = [] |
---|
510 | self._plen = len(prefix) |
---|
511 | if not prefix.endswith(os.sep): |
---|
512 | self._plen += 1 |
---|
513 | global functional |
---|
514 | if functional: |
---|
515 | self.dirname = "ftests" |
---|
516 | else: |
---|
517 | self.dirname = "tests" |
---|
518 | |
---|
519 | def visit(self, rx, dir, files): |
---|
520 | if os.path.split(dir)[1] != self.dirname: |
---|
521 | # Allow tests/ftests module rather than package. |
---|
522 | modfname = self.dirname + '.py' |
---|
523 | if modfname in files: |
---|
524 | path = os.path.join(dir, modfname) |
---|
525 | if match(rx, path): |
---|
526 | self.files.append(path) |
---|
527 | return |
---|
528 | return |
---|
529 | # ignore tests that aren't in packages |
---|
530 | if not "__init__.py" in files: |
---|
531 | if not files or files == ["CVS"]: |
---|
532 | return |
---|
533 | print "not a package", dir |
---|
534 | return |
---|
535 | |
---|
536 | # Put matching files in matches. If matches is non-empty, |
---|
537 | # then make sure that the package is importable. |
---|
538 | matches = [] |
---|
539 | for file in files: |
---|
540 | if file.startswith('test') and os.path.splitext(file)[-1] == '.py': |
---|
541 | path = os.path.join(dir, file) |
---|
542 | if match(rx, path): |
---|
543 | matches.append(path) |
---|
544 | |
---|
545 | # ignore tests when the package can't be imported, possibly due to |
---|
546 | # dependency failures. |
---|
547 | pkg = dir[self._plen:].replace(os.sep, '.') |
---|
548 | try: |
---|
549 | __import__(pkg) |
---|
550 | # We specifically do not want to catch ImportError since that's useful |
---|
551 | # information to know when running the tests. |
---|
552 | except RuntimeError, e: |
---|
553 | if VERBOSE: |
---|
554 | print "skipping %s because: %s" % (pkg, e) |
---|
555 | return |
---|
556 | else: |
---|
557 | self.files.extend(matches) |
---|
558 | |
---|
559 | def module_from_path(self, path): |
---|
560 | """Return the Python package name indicated by the filesystem path.""" |
---|
561 | assert path.endswith(".py") |
---|
562 | path = path[self._plen:-3] |
---|
563 | mod = path.replace(os.sep, ".") |
---|
564 | return mod |
---|
565 | |
---|
566 | def walk_with_symlinks(top, func, arg): |
---|
567 | """Like os.path.walk, but follows symlinks on POSIX systems. |
---|
568 | |
---|
569 | This could theoreticaly result in an infinite loop, if you create symlink |
---|
570 | cycles in your Zope sandbox, so don't do that. |
---|
571 | """ |
---|
572 | try: |
---|
573 | names = os.listdir(top) |
---|
574 | except os.error: |
---|
575 | return |
---|
576 | func(arg, top, names) |
---|
577 | exceptions = ('.', '..') |
---|
578 | for name in names: |
---|
579 | if name not in exceptions: |
---|
580 | name = os.path.join(top, name) |
---|
581 | if os.path.isdir(name): |
---|
582 | walk_with_symlinks(name, func, arg) |
---|
583 | |
---|
584 | def find_test_dir(dir): |
---|
585 | if os.path.exists(dir): |
---|
586 | return dir |
---|
587 | d = os.path.join(pathinit.libdir, dir) |
---|
588 | if os.path.exists(d): |
---|
589 | if os.path.isdir(d): |
---|
590 | return d |
---|
591 | raise ValueError("%s does not exist and %s is not a directory" |
---|
592 | % (dir, d)) |
---|
593 | raise ValueError("%s does not exist!" % dir) |
---|
594 | |
---|
595 | def find_tests(rx): |
---|
596 | global finder |
---|
597 | finder = TestFileFinder(pathinit.libdir) |
---|
598 | |
---|
599 | if TEST_DIRS: |
---|
600 | for d in TEST_DIRS: |
---|
601 | d = find_test_dir(d) |
---|
602 | walk_with_symlinks(d, finder.visit, rx) |
---|
603 | else: |
---|
604 | walk_with_symlinks(pathinit.libdir, finder.visit, rx) |
---|
605 | return finder.files |
---|
606 | |
---|
607 | def package_import(modname): |
---|
608 | mod = __import__(modname) |
---|
609 | for part in modname.split(".")[1:]: |
---|
610 | mod = getattr(mod, part) |
---|
611 | return mod |
---|
612 | |
---|
613 | class PseudoTestCase: |
---|
614 | """Minimal test case objects to create error reports. |
---|
615 | |
---|
616 | If test.py finds something that looks like it should be a test but |
---|
617 | can't load it or find its test suite, it will report an error |
---|
618 | using a PseudoTestCase. |
---|
619 | """ |
---|
620 | |
---|
621 | def __init__(self, name, descr=None): |
---|
622 | self.name = name |
---|
623 | self.descr = descr |
---|
624 | |
---|
625 | def shortDescription(self): |
---|
626 | return self.descr |
---|
627 | |
---|
628 | def __str__(self): |
---|
629 | return "Invalid Test (%s)" % self.name |
---|
630 | |
---|
631 | def get_suite(file, result): |
---|
632 | modname = finder.module_from_path(file) |
---|
633 | try: |
---|
634 | mod = package_import(modname) |
---|
635 | return mod.test_suite() |
---|
636 | except: |
---|
637 | result.addError(PseudoTestCase(modname), sys.exc_info()) |
---|
638 | return None |
---|
639 | |
---|
640 | def filter_testcases(s, rx): |
---|
641 | new = unittest.TestSuite() |
---|
642 | for test in s._tests: |
---|
643 | # See if the levels match |
---|
644 | dolevel = (LEVEL == 0) or LEVEL >= getattr(test, "level", 0) |
---|
645 | if not dolevel: |
---|
646 | continue |
---|
647 | if isinstance(test, unittest.TestCase): |
---|
648 | name = test.id() # Full test name: package.module.class.method |
---|
649 | name = name[1 + name.rfind("."):] # extract method name |
---|
650 | if not rx or match(rx, name): |
---|
651 | new.addTest(test) |
---|
652 | else: |
---|
653 | filtered = filter_testcases(test, rx) |
---|
654 | if filtered: |
---|
655 | new.addTest(filtered) |
---|
656 | return new |
---|
657 | |
---|
658 | def gui_runner(files, test_filter): |
---|
659 | if BUILD_INPLACE: |
---|
660 | utildir = os.path.join(os.getcwd(), "utilities") |
---|
661 | else: |
---|
662 | utildir = os.path.join(os.getcwd(), "..", "utilities") |
---|
663 | sys.path.append(utildir) |
---|
664 | import unittestgui |
---|
665 | suites = [] |
---|
666 | for file in files: |
---|
667 | suites.append(finder.module_from_path(file) + ".test_suite") |
---|
668 | |
---|
669 | suites = ", ".join(suites) |
---|
670 | minimal = (GUI == "minimal") |
---|
671 | unittestgui.main(suites, minimal) |
---|
672 | |
---|
673 | class TrackRefs: |
---|
674 | """Object to track reference counts across test runs.""" |
---|
675 | |
---|
676 | def __init__(self): |
---|
677 | self.type2count = {} |
---|
678 | self.type2all = {} |
---|
679 | |
---|
680 | def update(self): |
---|
681 | obs = sys.getobjects(0) |
---|
682 | type2count = {} |
---|
683 | type2all = {} |
---|
684 | for o in obs: |
---|
685 | all = sys.getrefcount(o) |
---|
686 | |
---|
687 | if type(o) is str and o == '<dummy key>': |
---|
688 | # avoid dictionary madness |
---|
689 | continue |
---|
690 | t = type(o) |
---|
691 | if t in type2count: |
---|
692 | type2count[t] += 1 |
---|
693 | type2all[t] += all |
---|
694 | else: |
---|
695 | type2count[t] = 1 |
---|
696 | type2all[t] = all |
---|
697 | |
---|
698 | ct = [(type2count[t] - self.type2count.get(t, 0), |
---|
699 | type2all[t] - self.type2all.get(t, 0), |
---|
700 | t) |
---|
701 | for t in type2count.iterkeys()] |
---|
702 | ct.sort() |
---|
703 | ct.reverse() |
---|
704 | printed = False |
---|
705 | for delta1, delta2, t in ct: |
---|
706 | if delta1 or delta2: |
---|
707 | if not printed: |
---|
708 | print "%-55s %8s %8s" % ('', 'insts', 'refs') |
---|
709 | printed = True |
---|
710 | print "%-55s %8d %8d" % (t, delta1, delta2) |
---|
711 | |
---|
712 | self.type2count = type2count |
---|
713 | self.type2all = type2all |
---|
714 | |
---|
715 | def runner(files, test_filter, debug): |
---|
716 | runner = ImmediateTestRunner(verbosity=VERBOSE, debug=DEBUG, |
---|
717 | progress=PROGRESS, profile=PROFILE, |
---|
718 | descriptions=False) |
---|
719 | suite = unittest.TestSuite() |
---|
720 | for file in files: |
---|
721 | s = get_suite(file, runner.result) |
---|
722 | # See if the levels match |
---|
723 | dolevel = (LEVEL == 0) or LEVEL >= getattr(s, "level", 0) |
---|
724 | if s is not None and dolevel: |
---|
725 | s = filter_testcases(s, test_filter) |
---|
726 | suite.addTest(s) |
---|
727 | try: |
---|
728 | r = runner.run(suite) |
---|
729 | if TIMESFN: |
---|
730 | r.print_times(open(TIMESFN, "w")) |
---|
731 | if VERBOSE: |
---|
732 | print "Wrote timing data to", TIMESFN |
---|
733 | if TIMETESTS: |
---|
734 | r.print_times(sys.stdout, TIMETESTS) |
---|
735 | except: |
---|
736 | if DEBUGGER: |
---|
737 | print "%s:" % (sys.exc_info()[0], ) |
---|
738 | print sys.exc_info()[1] |
---|
739 | pdb.post_mortem(sys.exc_info()[2]) |
---|
740 | else: |
---|
741 | raise |
---|
742 | |
---|
743 | def remove_stale_bytecode(arg, dirname, names): |
---|
744 | names = map(os.path.normcase, names) |
---|
745 | for name in names: |
---|
746 | if name.endswith(".pyc") or name.endswith(".pyo"): |
---|
747 | srcname = name[:-1] |
---|
748 | if srcname not in names: |
---|
749 | fullname = os.path.join(dirname, name) |
---|
750 | print "Removing stale bytecode file", fullname |
---|
751 | os.unlink(fullname) |
---|
752 | |
---|
753 | def main(module_filter, test_filter, libdir): |
---|
754 | if not KEEP_STALE_BYTECODE: |
---|
755 | os.path.walk(os.curdir, remove_stale_bytecode, None) |
---|
756 | |
---|
757 | configure_logging() |
---|
758 | |
---|
759 | # Initialize the path and cwd |
---|
760 | global pathinit |
---|
761 | pathinit = PathInit(BUILD, BUILD_INPLACE, libdir) |
---|
762 | |
---|
763 | files = find_tests(module_filter) |
---|
764 | files.sort() |
---|
765 | |
---|
766 | if GUI: |
---|
767 | gui_runner(files, test_filter) |
---|
768 | elif LOOP: |
---|
769 | if REFCOUNT: |
---|
770 | rc = sys.gettotalrefcount() |
---|
771 | track = TrackRefs() |
---|
772 | while True: |
---|
773 | runner(files, test_filter, DEBUG) |
---|
774 | gc.collect() |
---|
775 | if gc.garbage: |
---|
776 | print "GARBAGE:", len(gc.garbage), gc.garbage |
---|
777 | return |
---|
778 | if REFCOUNT: |
---|
779 | prev = rc |
---|
780 | rc = sys.gettotalrefcount() |
---|
781 | print "totalrefcount=%-8d change=%-6d" % (rc, rc - prev) |
---|
782 | track.update() |
---|
783 | else: |
---|
784 | runner(files, test_filter, DEBUG) |
---|
785 | |
---|
786 | os.chdir(pathinit.org_cwd) |
---|
787 | |
---|
788 | |
---|
789 | def configure_logging(): |
---|
790 | """Initialize the logging module.""" |
---|
791 | import logging.config |
---|
792 | |
---|
793 | # Get the log.ini file from the current directory instead of possibly |
---|
794 | # buried in the build directory. XXX This isn't perfect because if |
---|
795 | # log.ini specifies a log file, it'll be relative to the build directory. |
---|
796 | # Hmm... |
---|
797 | logini = os.path.abspath("log.ini") |
---|
798 | |
---|
799 | if os.path.exists(logini): |
---|
800 | logging.config.fileConfig(logini) |
---|
801 | else: |
---|
802 | logging.basicConfig() |
---|
803 | |
---|
804 | if os.environ.has_key("LOGGING"): |
---|
805 | level = int(os.environ["LOGGING"]) |
---|
806 | logging.getLogger().setLevel(level) |
---|
807 | |
---|
808 | |
---|
809 | def process_args(argv=None): |
---|
810 | import getopt |
---|
811 | global MODULE_FILTER |
---|
812 | global TEST_FILTER |
---|
813 | global VERBOSE |
---|
814 | global LOOP |
---|
815 | global GUI |
---|
816 | global TRACE |
---|
817 | global REFCOUNT |
---|
818 | global DEBUG |
---|
819 | global DEBUGGER |
---|
820 | global BUILD |
---|
821 | global LEVEL |
---|
822 | global LIBDIR |
---|
823 | global TIMESFN |
---|
824 | global TIMETESTS |
---|
825 | global PROGRESS |
---|
826 | global BUILD_INPLACE |
---|
827 | global KEEP_STALE_BYTECODE |
---|
828 | global TEST_DIRS |
---|
829 | global PROFILE |
---|
830 | global GC_THRESHOLD |
---|
831 | global GC_FLAGS |
---|
832 | global RUN_UNIT |
---|
833 | global RUN_FUNCTIONAL |
---|
834 | global PYCHECKER |
---|
835 | |
---|
836 | if argv is None: |
---|
837 | argv = sys.argv |
---|
838 | |
---|
839 | MODULE_FILTER = None |
---|
840 | TEST_FILTER = None |
---|
841 | VERBOSE = 0 |
---|
842 | LOOP = False |
---|
843 | GUI = False |
---|
844 | TRACE = False |
---|
845 | REFCOUNT = False |
---|
846 | DEBUG = False # Don't collect test results; simply let tests crash |
---|
847 | DEBUGGER = False |
---|
848 | BUILD = False |
---|
849 | BUILD_INPLACE = False |
---|
850 | GC_THRESHOLD = None |
---|
851 | gcdebug = 0 |
---|
852 | GC_FLAGS = [] |
---|
853 | LEVEL = 1 |
---|
854 | LIBDIR = None |
---|
855 | PROGRESS = False |
---|
856 | TIMESFN = None |
---|
857 | TIMETESTS = 0 |
---|
858 | KEEP_STALE_BYTECODE = 0 |
---|
859 | RUN_UNIT = True |
---|
860 | RUN_FUNCTIONAL = True |
---|
861 | TEST_DIRS = [] |
---|
862 | PROFILE = False |
---|
863 | PYCHECKER = False |
---|
864 | config_filename = 'test.config' |
---|
865 | |
---|
866 | # import the config file |
---|
867 | if os.path.isfile(config_filename): |
---|
868 | print 'Configuration file found.' |
---|
869 | execfile(config_filename, globals()) |
---|
870 | |
---|
871 | |
---|
872 | try: |
---|
873 | opts, args = getopt.getopt(argv[1:], "a:bBcdDfFg:G:hkl:LmMPprs:tTuUv", |
---|
874 | ["all", "help", "libdir=", "times=", |
---|
875 | "keepbytecode", "dir=", "build", |
---|
876 | "build-inplace", |
---|
877 | "at-level=", |
---|
878 | "pychecker", "debug", "pdebug", |
---|
879 | "gc-threshold=", "gc-option=", |
---|
880 | "loop", "gui", "minimal-gui", |
---|
881 | "profile", "progress", "refcount", "trace", |
---|
882 | "top-fifty", "verbose", |
---|
883 | ]) |
---|
884 | # fixme: add the long names |
---|
885 | # fixme: add the extra documentation |
---|
886 | # fixme: test for functional first! |
---|
887 | except getopt.error, msg: |
---|
888 | print msg |
---|
889 | print "Try `python %s -h' for more information." % argv[0] |
---|
890 | sys.exit(2) |
---|
891 | |
---|
892 | for k, v in opts: |
---|
893 | if k in ("-a", "--at-level"): |
---|
894 | LEVEL = int(v) |
---|
895 | elif k == "--all": |
---|
896 | LEVEL = 0 |
---|
897 | os.environ["COMPLAIN_IF_TESTS_MISSED"]='1' |
---|
898 | elif k in ("-b", "--build"): |
---|
899 | BUILD = True |
---|
900 | elif k in ("-B", "--build-inplace"): |
---|
901 | BUILD = BUILD_INPLACE = True |
---|
902 | elif k in("-c", "--pychecker"): |
---|
903 | PYCHECKER = True |
---|
904 | elif k in ("-d", "--debug"): |
---|
905 | DEBUG = True |
---|
906 | elif k in ("-D", "--pdebug"): |
---|
907 | DEBUG = True |
---|
908 | DEBUGGER = True |
---|
909 | elif k in ("-f", "--skip-unit"): |
---|
910 | RUN_UNIT = False |
---|
911 | elif k in ("-u", "--skip-functional"): |
---|
912 | RUN_FUNCTIONAL = False |
---|
913 | elif k == "-F": |
---|
914 | message = 'Unit plus functional is the default behaviour.' |
---|
915 | warnings.warn(message, DeprecationWarning) |
---|
916 | RUN_UNIT = True |
---|
917 | RUN_FUNCTIONAL = True |
---|
918 | elif k in ("-h", "--help"): |
---|
919 | print __doc__ |
---|
920 | sys.exit(0) |
---|
921 | elif k in ("-g", "--gc-threshold"): |
---|
922 | GC_THRESHOLD = int(v) |
---|
923 | elif k in ("-G", "--gc-option"): |
---|
924 | if not v.startswith("DEBUG_"): |
---|
925 | print "-G argument must be DEBUG_ flag, not", repr(v) |
---|
926 | sys.exit(1) |
---|
927 | GC_FLAGS.append(v) |
---|
928 | elif k in ('-k', '--keepbytecode'): |
---|
929 | KEEP_STALE_BYTECODE = 1 |
---|
930 | elif k in ('-l', '--libdir'): |
---|
931 | LIBDIR = v |
---|
932 | elif k in ("-L", "--loop"): |
---|
933 | LOOP = 1 |
---|
934 | elif k == "-m": |
---|
935 | GUI = "minimal" |
---|
936 | msg = "Use -M or --minimal-gui instead of -m." |
---|
937 | warnings.warn(msg, DeprecationWarning) |
---|
938 | elif k in ("-M", "--minimal-gui"): |
---|
939 | GUI = "minimal" |
---|
940 | elif k in ("-P", "--profile"): |
---|
941 | PROFILE = True |
---|
942 | elif k in ("-p", "--progress"): |
---|
943 | PROGRESS = True |
---|
944 | elif k in ("-r", "--refcount"): |
---|
945 | REFCOUNT = True |
---|
946 | elif k in ("-T", "--trace"): |
---|
947 | TRACE = True |
---|
948 | elif k in ("-t", "--top-fifty"): |
---|
949 | if not TIMETESTS: |
---|
950 | TIMETESTS = 50 |
---|
951 | elif k in ("-u", "--gui"): |
---|
952 | GUI = 1 |
---|
953 | elif k in ("-v", "--verbose"): |
---|
954 | VERBOSE += 1 |
---|
955 | elif k == "--times": |
---|
956 | try: |
---|
957 | TIMETESTS = int(v) |
---|
958 | except ValueError: |
---|
959 | # must be a filename to write |
---|
960 | TIMESFN = v |
---|
961 | elif k in ('-s', '--dir'): |
---|
962 | TEST_DIRS.append(v) |
---|
963 | |
---|
964 | if PYCHECKER: |
---|
965 | # make sure you have a recent version of pychecker |
---|
966 | if not os.environ.get("PYCHECKER"): |
---|
967 | os.environ["PYCHECKER"] = "-q" |
---|
968 | import pychecker.checker |
---|
969 | |
---|
970 | if REFCOUNT and not hasattr(sys, "gettotalrefcount"): |
---|
971 | print "-r ignored, because it needs a debug build of Python" |
---|
972 | REFCOUNT = False |
---|
973 | |
---|
974 | if sys.version_info < ( 2,3,2 ): |
---|
975 | print """\ |
---|
976 | ERROR: Your python version is not supported by Zope3. |
---|
977 | Zope3 needs Python 2.3.2 or greater. You are running:""" + sys.version |
---|
978 | sys.exit(1) |
---|
979 | |
---|
980 | if GC_THRESHOLD is not None: |
---|
981 | if GC_THRESHOLD == 0: |
---|
982 | gc.disable() |
---|
983 | print "gc disabled" |
---|
984 | else: |
---|
985 | gc.set_threshold(GC_THRESHOLD) |
---|
986 | print "gc threshold:", gc.get_threshold() |
---|
987 | |
---|
988 | if GC_FLAGS: |
---|
989 | val = 0 |
---|
990 | for flag in GC_FLAGS: |
---|
991 | v = getattr(gc, flag, None) |
---|
992 | if v is None: |
---|
993 | print "Unknown gc flag", repr(flag) |
---|
994 | print gc.set_debug.__doc__ |
---|
995 | sys.exit(1) |
---|
996 | val |= v |
---|
997 | gcdebug |= v |
---|
998 | |
---|
999 | if gcdebug: |
---|
1000 | gc.set_debug(gcdebug) |
---|
1001 | |
---|
1002 | if BUILD: |
---|
1003 | # Python 2.3 is more sane in its non -q output |
---|
1004 | if sys.hexversion >= 0x02030000: |
---|
1005 | qflag = "" |
---|
1006 | else: |
---|
1007 | qflag = "-q" |
---|
1008 | cmd = sys.executable + " setup.py " + qflag + " build" |
---|
1009 | if BUILD_INPLACE: |
---|
1010 | cmd += "_ext -i" |
---|
1011 | if VERBOSE: |
---|
1012 | print cmd |
---|
1013 | sts = os.system(cmd) |
---|
1014 | if sts: |
---|
1015 | print "Build failed", hex(sts) |
---|
1016 | sys.exit(1) |
---|
1017 | |
---|
1018 | k = [] |
---|
1019 | if RUN_UNIT: |
---|
1020 | k.append(False) |
---|
1021 | if RUN_FUNCTIONAL: |
---|
1022 | k.append(True) |
---|
1023 | |
---|
1024 | global functional |
---|
1025 | for functional in k: |
---|
1026 | |
---|
1027 | if VERBOSE: |
---|
1028 | kind = functional and "FUNCTIONAL" or "UNIT" |
---|
1029 | if LEVEL == 0: |
---|
1030 | print "Running %s tests at all levels" % kind |
---|
1031 | else: |
---|
1032 | print "Running %s tests at level %d" % (kind, LEVEL) |
---|
1033 | |
---|
1034 | # This was to avoid functional tests outside of z3, but this doesn't really |
---|
1035 | # work right. |
---|
1036 | ## if functional: |
---|
1037 | ## try: |
---|
1038 | ## from zope.app.tests.functional import FunctionalTestSetup |
---|
1039 | ## except ImportError: |
---|
1040 | ## raise |
---|
1041 | ## print ('Skipping functional tests: could not import ' |
---|
1042 | ## 'zope.app.tests.functional') |
---|
1043 | ## continue |
---|
1044 | |
---|
1045 | # XXX We want to change *visible* warnings into errors. The next |
---|
1046 | # line changes all warnings into errors, including warnings we |
---|
1047 | # normally never see. In particular, test_datetime does some |
---|
1048 | # short-integer arithmetic that overflows to long ints, and, by |
---|
1049 | # default, Python doesn't display the overflow warning that can |
---|
1050 | # be enabled when this happens. The next line turns that into an |
---|
1051 | # error instead. Guido suggests that a better to get what we're |
---|
1052 | # after is to replace warnings.showwarning() with our own thing |
---|
1053 | # that raises an error. |
---|
1054 | ## warnings.filterwarnings("error") |
---|
1055 | warnings.filterwarnings("ignore", module="logging") |
---|
1056 | |
---|
1057 | if args: |
---|
1058 | if len(args) > 1: |
---|
1059 | TEST_FILTER = args[1] |
---|
1060 | MODULE_FILTER = args[0] |
---|
1061 | try: |
---|
1062 | if TRACE: |
---|
1063 | # if the trace module is used, then we don't exit with |
---|
1064 | # status if on a false return value from main. |
---|
1065 | coverdir = os.path.join(os.getcwd(), "coverage") |
---|
1066 | import trace |
---|
1067 | ignoremods = ["os", "posixpath", "stat"] |
---|
1068 | tracer = trace.Trace(ignoredirs=[sys.prefix, sys.exec_prefix], |
---|
1069 | ignoremods=ignoremods, |
---|
1070 | trace=False, count=True) |
---|
1071 | |
---|
1072 | tracer.runctx("main(MODULE_FILTER, TEST_FILTER, LIBDIR)", |
---|
1073 | globals=globals(), locals=vars()) |
---|
1074 | r = tracer.results() |
---|
1075 | path = "/tmp/trace.%s" % os.getpid() |
---|
1076 | import cPickle |
---|
1077 | f = open(path, "wb") |
---|
1078 | cPickle.dump(r, f) |
---|
1079 | f.close() |
---|
1080 | print path |
---|
1081 | r.write_results(show_missing=True, |
---|
1082 | summary=True, coverdir=coverdir) |
---|
1083 | else: |
---|
1084 | bad = main(MODULE_FILTER, TEST_FILTER, LIBDIR) |
---|
1085 | if bad: |
---|
1086 | sys.exit(1) |
---|
1087 | except ImportError, err: |
---|
1088 | print err |
---|
1089 | print sys.path |
---|
1090 | raise |
---|
1091 | |
---|
1092 | |
---|
1093 | if __name__ == "__main__": |
---|
1094 | process_args() |
---|