OLD | NEW |
| (Empty) |
1 # -*- test-case-name: twisted.trial.test.test_reporter -*- | |
2 # | |
3 # Copyright (c) 2001-2007 Twisted Matrix Laboratories. | |
4 # See LICENSE for details. | |
5 # | |
6 # Maintainer: Jonathan Lange <jml@twistedmatrix.com> | |
7 | |
8 """ | |
9 Defines classes that handle the results of tests. | |
10 """ | |
11 | |
12 import sys, os | |
13 import time | |
14 import warnings | |
15 | |
16 from twisted.python import reflect, log | |
17 from twisted.python.components import proxyForInterface | |
18 from twisted.python.failure import Failure | |
19 from twisted.python.util import untilConcludes | |
20 from twisted.trial import itrial, util | |
21 | |
22 from zope.interface import implements | |
23 | |
24 pyunit = __import__('unittest') | |
25 | |
26 | |
27 class BrokenTestCaseWarning(Warning): | |
28 """emitted as a warning when an exception occurs in one of | |
29 setUp, tearDown, setUpClass, or tearDownClass""" | |
30 | |
31 | |
32 class SafeStream(object): | |
33 """ | |
34 Wraps a stream object so that all C{write} calls are wrapped in | |
35 L{untilConcludes}. | |
36 """ | |
37 | |
38 def __init__(self, original): | |
39 self.original = original | |
40 | |
41 def __getattr__(self, name): | |
42 return getattr(self.original, name) | |
43 | |
44 def write(self, *a, **kw): | |
45 return untilConcludes(self.original.write, *a, **kw) | |
46 | |
47 | |
48 class TestResult(pyunit.TestResult, object): | |
49 """ | |
50 Accumulates the results of several L{twisted.trial.unittest.TestCase}s. | |
51 | |
52 @ivar successes: count the number of successes achieved by the test run. | |
53 @type successes: C{int} | |
54 """ | |
55 implements(itrial.IReporter) | |
56 | |
57 def __init__(self): | |
58 super(TestResult, self).__init__() | |
59 self.skips = [] | |
60 self.expectedFailures = [] | |
61 self.unexpectedSuccesses = [] | |
62 self.successes = 0 | |
63 self._timings = [] | |
64 | |
65 def __repr__(self): | |
66 return ('<%s run=%d errors=%d failures=%d todos=%d dones=%d skips=%d>' | |
67 % (reflect.qual(self.__class__), self.testsRun, | |
68 len(self.errors), len(self.failures), | |
69 len(self.expectedFailures), len(self.skips), | |
70 len(self.unexpectedSuccesses))) | |
71 | |
72 def _getTime(self): | |
73 return time.time() | |
74 | |
75 def _getFailure(self, error): | |
76 """ | |
77 Convert a C{sys.exc_info()}-style tuple to a L{Failure}, if necessary. | |
78 """ | |
79 if isinstance(error, tuple): | |
80 return Failure(error[1], error[0], error[2]) | |
81 return error | |
82 | |
83 def startTest(self, test): | |
84 """This must be called before the given test is commenced. | |
85 | |
86 @type test: L{pyunit.TestCase} | |
87 """ | |
88 super(TestResult, self).startTest(test) | |
89 self._testStarted = self._getTime() | |
90 | |
91 def stopTest(self, test): | |
92 """This must be called after the given test is completed. | |
93 | |
94 @type test: L{pyunit.TestCase} | |
95 """ | |
96 super(TestResult, self).stopTest(test) | |
97 self._lastTime = self._getTime() - self._testStarted | |
98 | |
99 def addFailure(self, test, fail): | |
100 """Report a failed assertion for the given test. | |
101 | |
102 @type test: L{pyunit.TestCase} | |
103 @type fail: L{Failure} or L{tuple} | |
104 """ | |
105 self.failures.append((test, self._getFailure(fail))) | |
106 | |
107 def addError(self, test, error): | |
108 """Report an error that occurred while running the given test. | |
109 | |
110 @type test: L{pyunit.TestCase} | |
111 @type fail: L{Failure} or L{tuple} | |
112 """ | |
113 self.errors.append((test, self._getFailure(error))) | |
114 | |
115 def addSkip(self, test, reason): | |
116 """ | |
117 Report that the given test was skipped. | |
118 | |
119 In Trial, tests can be 'skipped'. Tests are skipped mostly because there | |
120 is some platform or configuration issue that prevents them from being | |
121 run correctly. | |
122 | |
123 @type test: L{pyunit.TestCase} | |
124 @type reason: L{str} | |
125 """ | |
126 self.skips.append((test, reason)) | |
127 | |
128 def addUnexpectedSuccess(self, test, todo): | |
129 """Report that the given test succeeded against expectations. | |
130 | |
131 In Trial, tests can be marked 'todo'. That is, they are expected to fail
. | |
132 When a test that is expected to fail instead succeeds, it should call | |
133 this method to report the unexpected success. | |
134 | |
135 @type test: L{pyunit.TestCase} | |
136 @type todo: L{unittest.Todo} | |
137 """ | |
138 # XXX - 'todo' should just be a string | |
139 self.unexpectedSuccesses.append((test, todo)) | |
140 | |
141 def addExpectedFailure(self, test, error, todo): | |
142 """Report that the given test failed, and was expected to do so. | |
143 | |
144 In Trial, tests can be marked 'todo'. That is, they are expected to fail
. | |
145 | |
146 @type test: L{pyunit.TestCase} | |
147 @type error: L{Failure} | |
148 @type todo: L{unittest.Todo} | |
149 """ | |
150 # XXX - 'todo' should just be a string | |
151 self.expectedFailures.append((test, error, todo)) | |
152 | |
153 def addSuccess(self, test): | |
154 """Report that the given test succeeded. | |
155 | |
156 @type test: L{pyunit.TestCase} | |
157 """ | |
158 self.successes += 1 | |
159 | |
160 def upDownError(self, method, error, warn, printStatus): | |
161 warnings.warn("upDownError is deprecated in Twisted 8.0.", | |
162 category=DeprecationWarning, stacklevel=3) | |
163 | |
164 def cleanupErrors(self, errs): | |
165 """Report an error that occurred during the cleanup between tests. | |
166 """ | |
167 warnings.warn("Cleanup errors are actual errors. Use addError. " | |
168 "Deprecated in Twisted 8.0", | |
169 category=DeprecationWarning, stacklevel=2) | |
170 | |
171 def startSuite(self, name): | |
172 warnings.warn("startSuite deprecated in Twisted 8.0", | |
173 category=DeprecationWarning, stacklevel=2) | |
174 | |
175 def endSuite(self, name): | |
176 warnings.warn("endSuite deprecated in Twisted 8.0", | |
177 category=DeprecationWarning, stacklevel=2) | |
178 | |
179 | |
180 def done(self): | |
181 """ | |
182 The test suite has finished running. | |
183 """ | |
184 | |
185 | |
186 | |
187 class TestResultDecorator(proxyForInterface(itrial.IReporter, | |
188 "_originalReporter")): | |
189 """ | |
190 Base class for TestResult decorators. | |
191 | |
192 @ivar _originalReporter: The wrapped instance of reporter. | |
193 @type _originalReporter: A provider of L{itrial.IReporter} | |
194 """ | |
195 | |
196 implements(itrial.IReporter) | |
197 | |
198 | |
199 | |
200 class UncleanWarningsReporterWrapper(TestResultDecorator): | |
201 """ | |
202 A wrapper for a reporter that converts L{util.DirtyReactorError}s | |
203 to warnings. | |
204 """ | |
205 implements(itrial.IReporter) | |
206 | |
207 def addError(self, test, error): | |
208 """ | |
209 If the error is a L{util.DirtyReactorError}, instead of | |
210 reporting it as a normal error, throw a warning. | |
211 """ | |
212 | |
213 if (isinstance(error, Failure) | |
214 and error.check(util.DirtyReactorAggregateError)): | |
215 warnings.warn(error.getErrorMessage()) | |
216 else: | |
217 self._originalReporter.addError(test, error) | |
218 | |
219 | |
220 | |
221 class _AdaptedReporter(TestResultDecorator): | |
222 """ | |
223 TestResult decorator that makes sure that addError only gets tests that | |
224 have been adapted with a particular test adapter. | |
225 """ | |
226 | |
227 def __init__(self, original, testAdapter): | |
228 """ | |
229 Construct an L{_AdaptedReporter}. | |
230 | |
231 @param original: An {itrial.IReporter}. | |
232 @param testAdapter: A callable that returns an L{itrial.ITestCase}. | |
233 """ | |
234 TestResultDecorator.__init__(self, original) | |
235 self.testAdapter = testAdapter | |
236 | |
237 | |
238 def addError(self, test, error): | |
239 """ | |
240 See L{itrial.IReporter}. | |
241 """ | |
242 test = self.testAdapter(test) | |
243 return self._originalReporter.addError(test, error) | |
244 | |
245 | |
246 def addExpectedFailure(self, test, failure, todo): | |
247 """ | |
248 See L{itrial.IReporter}. | |
249 """ | |
250 return self._originalReporter.addExpectedFailure( | |
251 self.testAdapter(test), failure, todo) | |
252 | |
253 | |
254 def addFailure(self, test, failure): | |
255 """ | |
256 See L{itrial.IReporter}. | |
257 """ | |
258 test = self.testAdapter(test) | |
259 return self._originalReporter.addFailure(test, failure) | |
260 | |
261 | |
262 def addSkip(self, test, skip): | |
263 """ | |
264 See L{itrial.IReporter}. | |
265 """ | |
266 test = self.testAdapter(test) | |
267 return self._originalReporter.addSkip(test, skip) | |
268 | |
269 | |
270 def addUnexpectedSuccess(self, test, todo): | |
271 """ | |
272 See L{itrial.IReporter}. | |
273 """ | |
274 test = self.testAdapter(test) | |
275 return self._originalReporter.addUnexpectedSuccess(test, todo) | |
276 | |
277 | |
278 def startTest(self, test): | |
279 """ | |
280 See L{itrial.IReporter}. | |
281 """ | |
282 return self._originalReporter.startTest(self.testAdapter(test)) | |
283 | |
284 | |
285 def stopTest(self, test): | |
286 """ | |
287 See L{itrial.IReporter}. | |
288 """ | |
289 return self._originalReporter.stopTest(self.testAdapter(test)) | |
290 | |
291 | |
292 | |
293 class Reporter(TestResult): | |
294 """ | |
295 A basic L{TestResult} with support for writing to a stream. | |
296 """ | |
297 | |
298 implements(itrial.IReporter) | |
299 | |
300 _separator = '-' * 79 | |
301 _doubleSeparator = '=' * 79 | |
302 | |
303 def __init__(self, stream=sys.stdout, tbformat='default', realtime=False): | |
304 super(Reporter, self).__init__() | |
305 self._stream = SafeStream(stream) | |
306 self.tbformat = tbformat | |
307 self.realtime = realtime | |
308 # The time when the first test was started. | |
309 self._startTime = None | |
310 | |
311 | |
312 def stream(self): | |
313 warnings.warn("stream is deprecated in Twisted 8.0.", | |
314 category=DeprecationWarning, stacklevel=4) | |
315 return self._stream | |
316 stream = property(stream) | |
317 | |
318 | |
319 def separator(self): | |
320 warnings.warn("separator is deprecated in Twisted 8.0.", | |
321 category=DeprecationWarning, stacklevel=4) | |
322 return self._separator | |
323 separator = property(separator) | |
324 | |
325 | |
326 def startTest(self, test): | |
327 """ | |
328 Called when a test begins to run. Records the time when it was first | |
329 called. | |
330 | |
331 @param test: L{ITestCase} | |
332 """ | |
333 super(Reporter, self).startTest(test) | |
334 if self._startTime is None: | |
335 self._startTime = time.time() | |
336 | |
337 | |
338 def addFailure(self, test, fail): | |
339 """ | |
340 Called when a test fails. If L{realtime} is set, then it prints the | |
341 error to the stream. | |
342 | |
343 @param test: L{ITestCase} that failed. | |
344 @param fail: L{failure.Failure} containing the error. | |
345 """ | |
346 super(Reporter, self).addFailure(test, fail) | |
347 if self.realtime: | |
348 fail = self.failures[-1][1] # guarantee it's a Failure | |
349 self._write(self._formatFailureTraceback(fail)) | |
350 | |
351 | |
352 def addError(self, test, error): | |
353 """ | |
354 Called when a test raises an error. If L{realtime} is set, then it | |
355 prints the error to the stream. | |
356 | |
357 @param test: L{ITestCase} that raised the error. | |
358 @param error: L{failure.Failure} containing the error. | |
359 """ | |
360 error = self._getFailure(error) | |
361 super(Reporter, self).addError(test, error) | |
362 if self.realtime: | |
363 error = self.errors[-1][1] # guarantee it's a Failure | |
364 self._write(self._formatFailureTraceback(error)) | |
365 | |
366 | |
367 def write(self, format, *args): | |
368 warnings.warn("write is deprecated in Twisted 8.0.", | |
369 category=DeprecationWarning, stacklevel=2) | |
370 self._write(format, *args) | |
371 | |
372 | |
373 def _write(self, format, *args): | |
374 """ | |
375 Safely write to the reporter's stream. | |
376 | |
377 @param format: A format string to write. | |
378 @param *args: The arguments for the format string. | |
379 """ | |
380 s = str(format) | |
381 assert isinstance(s, type('')) | |
382 if args: | |
383 self._stream.write(s % args) | |
384 else: | |
385 self._stream.write(s) | |
386 untilConcludes(self._stream.flush) | |
387 | |
388 | |
389 def writeln(self, format, *args): | |
390 warnings.warn("writeln is deprecated in Twisted 8.0.", | |
391 category=DeprecationWarning, stacklevel=2) | |
392 self._writeln(format, *args) | |
393 | |
394 | |
395 def _writeln(self, format, *args): | |
396 """ | |
397 Safely write a line to the reporter's stream. Newline is appended to | |
398 the format string. | |
399 | |
400 @param format: A format string to write. | |
401 @param *args: The arguments for the format string. | |
402 """ | |
403 self._write(format, *args) | |
404 self._write('\n') | |
405 | |
406 | |
407 def upDownError(self, method, error, warn, printStatus): | |
408 super(Reporter, self).upDownError(method, error, warn, printStatus) | |
409 if warn: | |
410 tbStr = self._formatFailureTraceback(error) | |
411 log.msg(tbStr) | |
412 msg = ("caught exception in %s, your TestCase is broken\n\n%s" | |
413 % (method, tbStr)) | |
414 warnings.warn(msg, BrokenTestCaseWarning, stacklevel=2) | |
415 | |
416 | |
417 def cleanupErrors(self, errs): | |
418 super(Reporter, self).cleanupErrors(errs) | |
419 warnings.warn("%s\n%s" % ("REACTOR UNCLEAN! traceback(s) follow: ", | |
420 self._formatFailureTraceback(errs)), | |
421 BrokenTestCaseWarning) | |
422 | |
423 | |
424 def _trimFrames(self, frames): | |
425 # when a method fails synchronously, the stack looks like this: | |
426 # [0]: defer.maybeDeferred() | |
427 # [1]: utils.runWithWarningsSuppressed() | |
428 # [2:-2]: code in the test method which failed | |
429 # [-1]: unittest.fail | |
430 | |
431 # when a method fails inside a Deferred (i.e., when the test method | |
432 # returns a Deferred, and that Deferred's errback fires), the stack | |
433 # captured inside the resulting Failure looks like this: | |
434 # [0]: defer.Deferred._runCallbacks | |
435 # [1:-2]: code in the testmethod which failed | |
436 # [-1]: unittest.fail | |
437 | |
438 # as a result, we want to trim either [maybeDeferred,runWWS] or | |
439 # [Deferred._runCallbacks] from the front, and trim the | |
440 # [unittest.fail] from the end. | |
441 | |
442 # There is also another case, when the test method is badly defined and | |
443 # contains extra arguments. | |
444 | |
445 newFrames = list(frames) | |
446 | |
447 if len(frames) < 2: | |
448 return newFrames | |
449 | |
450 first = newFrames[0] | |
451 second = newFrames[1] | |
452 if (first[0] == "maybeDeferred" | |
453 and os.path.splitext(os.path.basename(first[1]))[0] == 'defer' | |
454 and second[0] == "runWithWarningsSuppressed" | |
455 and os.path.splitext(os.path.basename(second[1]))[0] == 'utils'): | |
456 newFrames = newFrames[2:] | |
457 elif (first[0] == "_runCallbacks" | |
458 and os.path.splitext(os.path.basename(first[1]))[0] == 'defer'): | |
459 newFrames = newFrames[1:] | |
460 | |
461 if not newFrames: | |
462 # The method fails before getting called, probably an argument probl
em | |
463 return newFrames | |
464 | |
465 last = newFrames[-1] | |
466 if (last[0].startswith('fail') | |
467 and os.path.splitext(os.path.basename(last[1]))[0] == 'unittest'): | |
468 newFrames = newFrames[:-1] | |
469 | |
470 return newFrames | |
471 | |
472 | |
473 def _formatFailureTraceback(self, fail): | |
474 if isinstance(fail, str): | |
475 return fail.rstrip() + '\n' | |
476 fail.frames, frames = self._trimFrames(fail.frames), fail.frames | |
477 result = fail.getTraceback(detail=self.tbformat, elideFrameworkCode=True
) | |
478 fail.frames = frames | |
479 return result | |
480 | |
481 | |
482 def _printResults(self, flavour, errors, formatter): | |
483 """ | |
484 Print a group of errors to the stream. | |
485 | |
486 @param flavour: A string indicating the kind of error (e.g. 'TODO'). | |
487 @param errors: A list of errors, often L{failure.Failure}s, but | |
488 sometimes 'todo' errors. | |
489 @param formatter: A callable that knows how to format the errors. | |
490 """ | |
491 for content in errors: | |
492 self._writeln(self._doubleSeparator) | |
493 self._writeln('%s: %s' % (flavour, content[0].id())) | |
494 self._writeln('') | |
495 self._write(formatter(*(content[1:]))) | |
496 | |
497 | |
498 def _printExpectedFailure(self, error, todo): | |
499 return 'Reason: %r\n%s' % (todo.reason, | |
500 self._formatFailureTraceback(error)) | |
501 | |
502 | |
503 def _printUnexpectedSuccess(self, todo): | |
504 ret = 'Reason: %r\n' % (todo.reason,) | |
505 if todo.errors: | |
506 ret += 'Expected errors: %s\n' % (', '.join(todo.errors),) | |
507 return ret | |
508 | |
509 | |
510 def printErrors(self): | |
511 """ | |
512 Print all of the non-success results in full to the stream. | |
513 """ | |
514 warnings.warn("printErrors is deprecated in Twisted 8.0.", | |
515 category=DeprecationWarning, stacklevel=2) | |
516 self._printErrors() | |
517 | |
518 | |
519 def _printErrors(self): | |
520 """ | |
521 Print all of the non-success results to the stream in full. | |
522 """ | |
523 self._write('\n') | |
524 self._printResults('[SKIPPED]', self.skips, lambda x : '%s\n' % x) | |
525 self._printResults('[TODO]', self.expectedFailures, | |
526 self._printExpectedFailure) | |
527 self._printResults('[FAIL]', self.failures, | |
528 self._formatFailureTraceback) | |
529 self._printResults('[ERROR]', self.errors, | |
530 self._formatFailureTraceback) | |
531 self._printResults('[SUCCESS!?!]', self.unexpectedSuccesses, | |
532 self._printUnexpectedSuccess) | |
533 | |
534 | |
535 def _getSummary(self): | |
536 """ | |
537 Return a formatted count of tests status results. | |
538 """ | |
539 summaries = [] | |
540 for stat in ("skips", "expectedFailures", "failures", "errors", | |
541 "unexpectedSuccesses"): | |
542 num = len(getattr(self, stat)) | |
543 if num: | |
544 summaries.append('%s=%d' % (stat, num)) | |
545 if self.successes: | |
546 summaries.append('successes=%d' % (self.successes,)) | |
547 summary = (summaries and ' ('+', '.join(summaries)+')') or '' | |
548 return summary | |
549 | |
550 | |
551 def printSummary(self): | |
552 """ | |
553 Print a line summarising the test results to the stream. | |
554 """ | |
555 warnings.warn("printSummary is deprecated in Twisted 8.0.", | |
556 category=DeprecationWarning, stacklevel=2) | |
557 self._printSummary() | |
558 | |
559 | |
560 def _printSummary(self): | |
561 """ | |
562 Print a line summarising the test results to the stream. | |
563 """ | |
564 summary = self._getSummary() | |
565 if self.wasSuccessful(): | |
566 status = "PASSED" | |
567 else: | |
568 status = "FAILED" | |
569 self._write("%s%s\n", status, summary) | |
570 | |
571 | |
572 def done(self): | |
573 """ | |
574 Summarize the result of the test run. | |
575 | |
576 The summary includes a report of all of the errors, todos, skips and | |
577 so forth that occurred during the run. It also includes the number of | |
578 tests that were run and how long it took to run them (not including | |
579 load time). | |
580 | |
581 Expects that L{_printErrors}, L{_writeln}, L{_write}, L{_printSummary} | |
582 and L{_separator} are all implemented. | |
583 """ | |
584 self._printErrors() | |
585 self._writeln(self._separator) | |
586 if self._startTime is not None: | |
587 self._writeln('Ran %d tests in %.3fs', self.testsRun, | |
588 time.time() - self._startTime) | |
589 self._write('\n') | |
590 self._printSummary() | |
591 | |
592 | |
593 | |
594 class MinimalReporter(Reporter): | |
595 """ | |
596 A minimalist reporter that prints only a summary of the test result, in | |
597 the form of (timeTaken, #tests, #tests, #errors, #failures, #skips). | |
598 """ | |
599 | |
600 def _printErrors(self): | |
601 """ | |
602 Don't print a detailed summary of errors. We only care about the | |
603 counts. | |
604 """ | |
605 | |
606 | |
607 def _printSummary(self): | |
608 """ | |
609 Print out a one-line summary of the form: | |
610 '%(runtime) %(number_of_tests) %(number_of_tests) %(num_errors) | |
611 %(num_failures) %(num_skips)' | |
612 """ | |
613 numTests = self.testsRun | |
614 t = (self._startTime - self._getTime(), numTests, numTests, | |
615 len(self.errors), len(self.failures), len(self.skips)) | |
616 self._writeln(' '.join(map(str, t))) | |
617 | |
618 | |
619 | |
620 class TextReporter(Reporter): | |
621 """ | |
622 Simple reporter that prints a single character for each test as it runs, | |
623 along with the standard Trial summary text. | |
624 """ | |
625 | |
626 def addSuccess(self, test): | |
627 super(TextReporter, self).addSuccess(test) | |
628 self._write('.') | |
629 | |
630 | |
631 def addError(self, *args): | |
632 super(TextReporter, self).addError(*args) | |
633 self._write('E') | |
634 | |
635 | |
636 def addFailure(self, *args): | |
637 super(TextReporter, self).addFailure(*args) | |
638 self._write('F') | |
639 | |
640 | |
641 def addSkip(self, *args): | |
642 super(TextReporter, self).addSkip(*args) | |
643 self._write('S') | |
644 | |
645 | |
646 def addExpectedFailure(self, *args): | |
647 super(TextReporter, self).addExpectedFailure(*args) | |
648 self._write('T') | |
649 | |
650 | |
651 def addUnexpectedSuccess(self, *args): | |
652 super(TextReporter, self).addUnexpectedSuccess(*args) | |
653 self._write('!') | |
654 | |
655 | |
656 | |
657 class VerboseTextReporter(Reporter): | |
658 """ | |
659 A verbose reporter that prints the name of each test as it is running. | |
660 | |
661 Each line is printed with the name of the test, followed by the result of | |
662 that test. | |
663 """ | |
664 | |
665 # This is actually the bwverbose option | |
666 | |
667 def startTest(self, tm): | |
668 self._write('%s ... ', tm.id()) | |
669 super(VerboseTextReporter, self).startTest(tm) | |
670 | |
671 | |
672 def addSuccess(self, test): | |
673 super(VerboseTextReporter, self).addSuccess(test) | |
674 self._write('[OK]') | |
675 | |
676 | |
677 def addError(self, *args): | |
678 super(VerboseTextReporter, self).addError(*args) | |
679 self._write('[ERROR]') | |
680 | |
681 | |
682 def addFailure(self, *args): | |
683 super(VerboseTextReporter, self).addFailure(*args) | |
684 self._write('[FAILURE]') | |
685 | |
686 | |
687 def addSkip(self, *args): | |
688 super(VerboseTextReporter, self).addSkip(*args) | |
689 self._write('[SKIPPED]') | |
690 | |
691 | |
692 def addExpectedFailure(self, *args): | |
693 super(VerboseTextReporter, self).addExpectedFailure(*args) | |
694 self._write('[TODO]') | |
695 | |
696 | |
697 def addUnexpectedSuccess(self, *args): | |
698 super(VerboseTextReporter, self).addUnexpectedSuccess(*args) | |
699 self._write('[SUCCESS!?!]') | |
700 | |
701 | |
702 def stopTest(self, test): | |
703 super(VerboseTextReporter, self).stopTest(test) | |
704 self._write('\n') | |
705 | |
706 | |
707 | |
708 class TimingTextReporter(VerboseTextReporter): | |
709 """ | |
710 Prints out each test as it is running, followed by the time taken for each | |
711 test to run. | |
712 """ | |
713 | |
714 def stopTest(self, method): | |
715 """ | |
716 Mark the test as stopped, and write the time it took to run the test | |
717 to the stream. | |
718 """ | |
719 super(TimingTextReporter, self).stopTest(method) | |
720 self._write("(%.03f secs)\n" % self._lastTime) | |
721 | |
722 | |
723 | |
724 class _AnsiColorizer(object): | |
725 """ | |
726 A colorizer is an object that loosely wraps around a stream, allowing | |
727 callers to write text to the stream in a particular color. | |
728 | |
729 Colorizer classes must implement C{supported()} and C{write(text, color)}. | |
730 """ | |
731 _colors = dict(black=30, red=31, green=32, yellow=33, | |
732 blue=34, magenta=35, cyan=36, white=37) | |
733 | |
734 def __init__(self, stream): | |
735 self.stream = stream | |
736 | |
737 def supported(self): | |
738 """ | |
739 A class method that returns True if the current platform supports | |
740 coloring terminal output using this method. Returns False otherwise. | |
741 """ | |
742 # assuming stderr | |
743 # isatty() returns False when SSHd into Win32 machine | |
744 if 'CYGWIN' in os.environ: | |
745 return True | |
746 if not sys.stderr.isatty(): | |
747 return False # auto color only on TTYs | |
748 try: | |
749 import curses | |
750 curses.setupterm() | |
751 return curses.tigetnum("colors") > 2 | |
752 except: | |
753 # guess false in case of error | |
754 return False | |
755 supported = classmethod(supported) | |
756 | |
757 def write(self, text, color): | |
758 """ | |
759 Write the given text to the stream in the given color. | |
760 | |
761 @param text: Text to be written to the stream. | |
762 | |
763 @param color: A string label for a color. e.g. 'red', 'white'. | |
764 """ | |
765 color = self._colors[color] | |
766 self.stream.write('\x1b[%s;1m%s\x1b[0m' % (color, text)) | |
767 | |
768 | |
769 class _Win32Colorizer(object): | |
770 """ | |
771 See _AnsiColorizer docstring. | |
772 """ | |
773 def __init__(self, stream): | |
774 from win32console import GetStdHandle, STD_OUTPUT_HANDLE, \ | |
775 FOREGROUND_RED, FOREGROUND_BLUE, FOREGROUND_GREEN, \ | |
776 FOREGROUND_INTENSITY | |
777 red, green, blue, bold = (FOREGROUND_RED, FOREGROUND_GREEN, | |
778 FOREGROUND_BLUE, FOREGROUND_INTENSITY) | |
779 self.stream = stream | |
780 self.screenBuffer = GetStdHandle(STD_OUTPUT_HANDLE) | |
781 self._colors = { | |
782 'normal': red | green | blue, | |
783 'red': red | bold, | |
784 'green': green | bold, | |
785 'blue': blue | bold, | |
786 'yellow': red | green | bold, | |
787 'magenta': red | blue | bold, | |
788 'cyan': green | blue | bold, | |
789 'white': red | green | blue | bold | |
790 } | |
791 | |
792 def supported(self): | |
793 try: | |
794 import win32console | |
795 screenBuffer = win32console.GetStdHandle( | |
796 win32console.STD_OUTPUT_HANDLE) | |
797 except ImportError: | |
798 return False | |
799 import pywintypes | |
800 try: | |
801 screenBuffer.SetConsoleTextAttribute( | |
802 win32console.FOREGROUND_RED | | |
803 win32console.FOREGROUND_GREEN | | |
804 win32console.FOREGROUND_BLUE) | |
805 except pywintypes.error: | |
806 return False | |
807 else: | |
808 return True | |
809 supported = classmethod(supported) | |
810 | |
811 def write(self, text, color): | |
812 color = self._colors[color] | |
813 self.screenBuffer.SetConsoleTextAttribute(color) | |
814 self.stream.write(text) | |
815 self.screenBuffer.SetConsoleTextAttribute(self._colors['normal']) | |
816 | |
817 | |
818 class _NullColorizer(object): | |
819 """ | |
820 See _AnsiColorizer docstring. | |
821 """ | |
822 def __init__(self, stream): | |
823 self.stream = stream | |
824 | |
825 def supported(self): | |
826 return True | |
827 supported = classmethod(supported) | |
828 | |
829 def write(self, text, color): | |
830 self.stream.write(text) | |
831 | |
832 | |
833 | |
834 class TreeReporter(Reporter): | |
835 """ | |
836 Print out the tests in the form a tree. | |
837 | |
838 Tests are indented according to which class and module they belong. | |
839 Results are printed in ANSI color. | |
840 """ | |
841 | |
842 currentLine = '' | |
843 indent = ' ' | |
844 columns = 79 | |
845 | |
846 FAILURE = 'red' | |
847 ERROR = 'red' | |
848 TODO = 'blue' | |
849 SKIP = 'blue' | |
850 TODONE = 'red' | |
851 SUCCESS = 'green' | |
852 | |
853 def __init__(self, stream=sys.stdout, tbformat='default', realtime=False): | |
854 super(TreeReporter, self).__init__(stream, tbformat, realtime) | |
855 self._lastTest = [] | |
856 for colorizer in [_Win32Colorizer, _AnsiColorizer, _NullColorizer]: | |
857 if colorizer.supported(): | |
858 self._colorizer = colorizer(stream) | |
859 break | |
860 | |
861 def getDescription(self, test): | |
862 """ | |
863 Return the name of the method which 'test' represents. This is | |
864 what gets displayed in the leaves of the tree. | |
865 | |
866 e.g. getDescription(TestCase('test_foo')) ==> test_foo | |
867 """ | |
868 return test.id().split('.')[-1] | |
869 | |
870 def addSuccess(self, test): | |
871 super(TreeReporter, self).addSuccess(test) | |
872 self.endLine('[OK]', self.SUCCESS) | |
873 | |
874 def addError(self, *args): | |
875 super(TreeReporter, self).addError(*args) | |
876 self.endLine('[ERROR]', self.ERROR) | |
877 | |
878 def addFailure(self, *args): | |
879 super(TreeReporter, self).addFailure(*args) | |
880 self.endLine('[FAIL]', self.FAILURE) | |
881 | |
882 def addSkip(self, *args): | |
883 super(TreeReporter, self).addSkip(*args) | |
884 self.endLine('[SKIPPED]', self.SKIP) | |
885 | |
886 def addExpectedFailure(self, *args): | |
887 super(TreeReporter, self).addExpectedFailure(*args) | |
888 self.endLine('[TODO]', self.TODO) | |
889 | |
890 def addUnexpectedSuccess(self, *args): | |
891 super(TreeReporter, self).addUnexpectedSuccess(*args) | |
892 self.endLine('[SUCCESS!?!]', self.TODONE) | |
893 | |
894 def _write(self, format, *args): | |
895 if args: | |
896 format = format % args | |
897 self.currentLine = format | |
898 super(TreeReporter, self)._write(self.currentLine) | |
899 | |
900 | |
901 def _getPreludeSegments(self, testID): | |
902 """ | |
903 Return a list of all non-leaf segments to display in the tree. | |
904 | |
905 Normally this is the module and class name. | |
906 """ | |
907 segments = testID.split('.')[:-1] | |
908 if len(segments) == 0: | |
909 return segments | |
910 segments = [ | |
911 seg for seg in '.'.join(segments[:-1]), segments[-1] | |
912 if len(seg) > 0] | |
913 return segments | |
914 | |
915 | |
916 def _testPrelude(self, testID): | |
917 """ | |
918 Write the name of the test to the stream, indenting it appropriately. | |
919 | |
920 If the test is the first test in a new 'branch' of the tree, also | |
921 write all of the parents in that branch. | |
922 """ | |
923 segments = self._getPreludeSegments(testID) | |
924 indentLevel = 0 | |
925 for seg in segments: | |
926 if indentLevel < len(self._lastTest): | |
927 if seg != self._lastTest[indentLevel]: | |
928 self._write('%s%s\n' % (self.indent * indentLevel, seg)) | |
929 else: | |
930 self._write('%s%s\n' % (self.indent * indentLevel, seg)) | |
931 indentLevel += 1 | |
932 self._lastTest = segments | |
933 | |
934 | |
935 def cleanupErrors(self, errs): | |
936 self._colorizer.write(' cleanup errors', self.ERROR) | |
937 self.endLine('[ERROR]', self.ERROR) | |
938 super(TreeReporter, self).cleanupErrors(errs) | |
939 | |
940 def upDownError(self, method, error, warn, printStatus): | |
941 self._colorizer.write(" %s" % method, self.ERROR) | |
942 if printStatus: | |
943 self.endLine('[ERROR]', self.ERROR) | |
944 super(TreeReporter, self).upDownError(method, error, warn, printStatus) | |
945 | |
946 def startTest(self, test): | |
947 """ | |
948 Called when C{test} starts. Writes the tests name to the stream using | |
949 a tree format. | |
950 """ | |
951 self._testPrelude(test.id()) | |
952 self._write('%s%s ... ' % (self.indent * (len(self._lastTest)), | |
953 self.getDescription(test))) | |
954 super(TreeReporter, self).startTest(test) | |
955 | |
956 | |
957 def endLine(self, message, color): | |
958 """ | |
959 Print 'message' in the given color. | |
960 | |
961 @param message: A string message, usually '[OK]' or something similar. | |
962 @param color: A string color, 'red', 'green' and so forth. | |
963 """ | |
964 spaces = ' ' * (self.columns - len(self.currentLine) - len(message)) | |
965 super(TreeReporter, self)._write(spaces) | |
966 self._colorizer.write(message, color) | |
967 super(TreeReporter, self)._write("\n") | |
968 | |
969 | |
970 def _printSummary(self): | |
971 """ | |
972 Print a line summarising the test results to the stream, and color the | |
973 status result. | |
974 """ | |
975 summary = self._getSummary() | |
976 if self.wasSuccessful(): | |
977 status = "PASSED" | |
978 color = self.SUCCESS | |
979 else: | |
980 status = "FAILED" | |
981 color = self.FAILURE | |
982 self._colorizer.write(status, color) | |
983 self._write("%s\n", summary) | |
OLD | NEW |