1
# -*- test-case-name: twisted.trial.test.test_reporter -*-
3
# Copyright (c) 2001-2004 Twisted Matrix Laboratories.
4
# See LICENSE for details.
6
# Maintainer: Jonathan Lange <jml@twistedmatrix.com>
8
"""Defines classes that handle the results of tests.
10
API Stability: Unstable
17
from twisted.python import reflect, failure, log
18
from twisted.python.util import untilConcludes
19
from twisted.trial import itrial
20
import zope.interface as zi
22
pyunit = __import__('unittest')
25
class BrokenTestCaseWarning(Warning):
26
"""emitted as a warning when an exception occurs in one of
27
setUp, tearDown, setUpClass, or tearDownClass"""
30
class SafeStream(object):
32
Wraps a stream object so that all C{write} calls are wrapped in
36
def __init__(self, original):
37
self.original = original
39
def __getattr__(self, name):
40
return getattr(self.original, name)
42
def write(self, *a, **kw):
43
return untilConcludes(self.original.write, *a, **kw)
46
class TestResult(pyunit.TestResult, object):
47
"""Accumulates the results of several L{twisted.trial.unittest.TestCase}s.
51
super(TestResult, self).__init__()
53
self.expectedFailures = []
54
self.unexpectedSuccesses = []
59
return ('<%s run=%d errors=%d failures=%d todos=%d dones=%d skips=%d>'
60
% (reflect.qual(self.__class__), self.testsRun,
61
len(self.errors), len(self.failures),
62
len(self.expectedFailures), len(self.skips),
63
len(self.unexpectedSuccesses)))
68
def startTest(self, test):
69
"""This must be called before the given test is commenced.
71
@type test: L{pyunit.TestCase}
73
super(TestResult, self).startTest(test)
74
self._testStarted = self._getTime()
76
def stopTest(self, test):
77
"""This must be called after the given test is completed.
79
@type test: L{pyunit.TestCase}
81
super(TestResult, self).stopTest(test)
82
self._lastTime = self._getTime() - self._testStarted
84
def addFailure(self, test, fail):
85
"""Report a failed assertion for the given test.
87
@type test: L{pyunit.TestCase}
88
@type fail: L{failure.Failure} or L{tuple}
90
if isinstance(fail, tuple):
91
fail = failure.Failure(fail[1], fail[0], fail[2])
92
self.failures.append((test, fail))
94
def addError(self, test, error):
95
"""Report an error that occurred while running the given test.
97
@type test: L{pyunit.TestCase}
98
@type fail: L{failure.Failure} or L{tuple}
100
if isinstance(error, tuple):
101
error = failure.Failure(error[1], error[0], error[2])
102
self.errors.append((test, error))
104
def addSkip(self, test, reason):
106
Report that the given test was skipped.
108
In Trial, tests can be 'skipped'. Tests are skipped mostly because there
109
is some platform or configuration issue that prevents them from being
112
@type test: L{pyunit.TestCase}
115
self.skips.append((test, reason))
117
def addUnexpectedSuccess(self, test, todo):
118
"""Report that the given test succeeded against expectations.
120
In Trial, tests can be marked 'todo'. That is, they are expected to fail.
121
When a test that is expected to fail instead succeeds, it should call
122
this method to report the unexpected success.
124
@type test: L{pyunit.TestCase}
125
@type todo: L{unittest.Todo}
127
# XXX - 'todo' should just be a string
128
self.unexpectedSuccesses.append((test, todo))
130
def addExpectedFailure(self, test, error, todo):
131
"""Report that the given test succeeded against expectations.
133
In Trial, tests can be marked 'todo'. That is, they are expected to fail.
135
@type test: L{pyunit.TestCase}
136
@type error: L{failure.Failure}
137
@type todo: L{unittest.Todo}
139
# XXX - 'todo' should just be a string
140
self.expectedFailures.append((test, error, todo))
142
def addSuccess(self, test):
143
"""Report that the given test succeeded.
145
@type test: L{pyunit.TestCase}
147
self.successes.append((test,))
149
def upDownError(self, method, error, warn, printStatus):
152
def cleanupErrors(self, errs):
153
"""Report an error that occurred during the cleanup between tests.
155
# XXX - deprecate this method, we don't need it any more
157
def startSuite(self, name):
158
# XXX - these should be removed, but not in this branch
161
def endSuite(self, name):
162
# XXX - these should be removed, but not in this branch
166
class Reporter(TestResult):
167
zi.implements(itrial.IReporter)
170
doubleSeparator = '=' * 79
172
def __init__(self, stream=sys.stdout, tbformat='default', realtime=False):
173
super(Reporter, self).__init__()
174
self.stream = SafeStream(stream)
175
self.tbformat = tbformat
176
self.realtime = realtime
178
def startTest(self, test):
179
super(Reporter, self).startTest(test)
181
def addFailure(self, test, fail):
182
super(Reporter, self).addFailure(test, fail)
184
fail = self.failures[-1][1] # guarantee it's a Failure
185
self.write(self._formatFailureTraceback(fail))
187
def addError(self, test, error):
188
super(Reporter, self).addError(test, error)
190
error = self.errors[-1][1] # guarantee it's a Failure
191
self.write(self._formatFailureTraceback(error))
193
def write(self, format, *args):
195
assert isinstance(s, type(''))
197
self.stream.write(s % args)
200
untilConcludes(self.stream.flush)
202
def writeln(self, format, *args):
203
self.write(format, *args)
206
def upDownError(self, method, error, warn, printStatus):
207
super(Reporter, self).upDownError(method, error, warn, printStatus)
209
tbStr = self._formatFailureTraceback(error)
211
msg = ("caught exception in %s, your TestCase is broken\n\n%s"
213
warnings.warn(msg, BrokenTestCaseWarning, stacklevel=2)
215
def cleanupErrors(self, errs):
216
super(Reporter, self).cleanupErrors(errs)
217
warnings.warn("%s\n%s" % ("REACTOR UNCLEAN! traceback(s) follow: ",
218
self._formatFailureTraceback(errs)),
219
BrokenTestCaseWarning)
221
def _trimFrames(self, frames):
222
# when a method fails synchronously, the stack looks like this:
223
# [0]: defer.maybeDeferred()
224
# [1]: utils.runWithWarningsSuppressed()
225
# [2:-2]: code in the test method which failed
226
# [-1]: unittest.fail
228
# when a method fails inside a Deferred (i.e., when the test method
229
# returns a Deferred, and that Deferred's errback fires), the stack
230
# captured inside the resulting Failure looks like this:
231
# [0]: defer.Deferred._runCallbacks
232
# [1:-2]: code in the testmethod which failed
233
# [-1]: unittest.fail
235
# as a result, we want to trim either [maybeDeferred,runWWS] or
236
# [Deferred._runCallbacks] from the front, and trim the
237
# [unittest.fail] from the end.
239
newFrames = list(frames)
245
second = newFrames[1]
246
if (first[0] == "maybeDeferred"
247
and os.path.splitext(os.path.basename(first[1]))[0] == 'defer'
248
and second[0] == "runWithWarningsSuppressed"
249
and os.path.splitext(os.path.basename(second[1]))[0] == 'utils'):
250
newFrames = newFrames[2:]
251
elif (first[0] == "_runCallbacks"
252
and os.path.splitext(os.path.basename(first[1]))[0] == 'defer'):
253
newFrames = newFrames[1:]
256
if (last[0].startswith('fail')
257
and os.path.splitext(os.path.basename(last[1]))[0] == 'unittest'):
258
newFrames = newFrames[:-1]
262
def _formatFailureTraceback(self, fail):
263
if isinstance(fail, str):
264
return fail.rstrip() + '\n'
265
fail.frames, frames = self._trimFrames(fail.frames), fail.frames
266
result = fail.getTraceback(detail=self.tbformat, elideFrameworkCode=True)
270
def _printResults(self, flavour, errors, formatter):
271
for content in errors:
272
self.writeln(self.doubleSeparator)
273
self.writeln('%s: %s' % (flavour, content[0].id()))
275
self.write(formatter(*(content[1:])))
277
def _printExpectedFailure(self, error, todo):
278
return 'Reason: %r\n%s' % (todo.reason,
279
self._formatFailureTraceback(error))
281
def _printUnexpectedSuccess(self, todo):
282
ret = 'Reason: %r\n' % (todo.reason,)
284
ret += 'Expected errors: %s\n' % (', '.join(todo.errors),)
287
def printErrors(self):
288
"""Print all of the non-success results in full to the stream.
291
self._printResults('[SKIPPED]', self.skips, lambda x : '%s\n' % x)
292
self._printResults('[TODO]', self.expectedFailures,
293
self._printExpectedFailure)
294
self._printResults('[FAIL]', self.failures,
295
self._formatFailureTraceback)
296
self._printResults('[ERROR]', self.errors,
297
self._formatFailureTraceback)
298
self._printResults('[SUCCESS!?!]', self.unexpectedSuccesses,
299
self._printUnexpectedSuccess)
301
def printSummary(self):
302
"""Print a line summarising the test results to the stream.
305
for stat in ("skips", "expectedFailures", "failures", "errors",
306
"unexpectedSuccesses", "successes"):
307
num = len(getattr(self, stat))
309
summaries.append('%s=%d' % (stat, num))
310
summary = (summaries and ' ('+', '.join(summaries)+')') or ''
311
if not self.wasSuccessful():
315
self.write("%s%s\n", status, summary)
318
class MinimalReporter(Reporter):
319
"""A minimalist reporter that prints only a summary of the test result,
320
in the form of (timeTaken, #tests, #tests, #errors, #failures, #skips).
325
def startTest(self, test):
326
super(MinimalReporter, self).startTest(test)
327
if self._runStarted is None:
328
self._runStarted = self._getTime()
330
def printErrors(self):
333
def printSummary(self):
334
numTests = self.testsRun
335
t = (self._runStarted - self._getTime(), numTests, numTests,
336
len(self.errors), len(self.failures), len(self.skips))
337
self.writeln(' '.join(map(str, t)))
340
class TextReporter(Reporter):
342
Simple reporter that prints a single character for each test as it runs,
343
along with the standard Trial summary text.
346
def addSuccess(self, test):
347
super(TextReporter, self).addSuccess(test)
350
def addError(self, *args):
351
super(TextReporter, self).addError(*args)
354
def addFailure(self, *args):
355
super(TextReporter, self).addFailure(*args)
358
def addSkip(self, *args):
359
super(TextReporter, self).addSkip(*args)
362
def addExpectedFailure(self, *args):
363
super(TextReporter, self).addExpectedFailure(*args)
366
def addUnexpectedSuccess(self, *args):
367
super(TextReporter, self).addUnexpectedSuccess(*args)
371
class VerboseTextReporter(Reporter):
373
A verbose reporter that prints the name of each test as it is running.
375
Each line is printed with the name of the test, followed by the result of
379
# This is actually the bwverbose option
381
def startTest(self, tm):
382
self.write('%s ... ', tm.id())
383
super(VerboseTextReporter, self).startTest(tm)
385
def addSuccess(self, test):
386
super(VerboseTextReporter, self).addSuccess(test)
389
def addError(self, *args):
390
super(VerboseTextReporter, self).addError(*args)
391
self.write('[ERROR]')
393
def addFailure(self, *args):
394
super(VerboseTextReporter, self).addFailure(*args)
395
self.write('[FAILURE]')
397
def addSkip(self, *args):
398
super(VerboseTextReporter, self).addSkip(*args)
399
self.write('[SKIPPED]')
401
def addExpectedFailure(self, *args):
402
super(VerboseTextReporter, self).addExpectedFailure(*args)
405
def addUnexpectedSuccess(self, *args):
406
super(VerboseTextReporter, self).addUnexpectedSuccess(*args)
407
self.write('[SUCCESS!?!]')
409
def stopTest(self, test):
410
super(VerboseTextReporter, self).stopTest(test)
414
class TimingTextReporter(VerboseTextReporter):
415
"""Prints out each test as it is running, followed by the time taken for each
419
def stopTest(self, method):
420
super(TimingTextReporter, self).stopTest(method)
421
self.write("(%.03f secs)\n" % self._lastTime)
424
class _AnsiColorizer(object):
426
A colorizer is an object that loosely wraps around a stream, allowing
427
callers to write text to the stream in a particular color.
429
Colorizer classes must implement C{supported()} and C{write(text, color)}.
431
_colors = dict(black=30, red=31, green=32, yellow=33,
432
blue=34, magenta=35, cyan=36, white=37)
434
def __init__(self, stream):
439
A class method that returns True if the current platform supports
440
coloring terminal output using this method. Returns False otherwise.
443
# isatty() returns False when SSHd into Win32 machine
444
if 'CYGWIN' in os.environ:
446
if not sys.stderr.isatty():
447
return False # auto color only on TTYs
451
return curses.tigetnum("colors") > 2
453
# guess false in case of error
455
supported = classmethod(supported)
457
def write(self, text, color):
459
Write the given text to the stream in the given color.
461
@param text: Text to be written to the stream.
463
@param color: A string label for a color. e.g. 'red', 'white'.
465
color = self._colors[color]
466
self.stream.write('\x1b[%s;1m%s\x1b[0m' % (color, text))
469
class _Win32Colorizer(object):
471
See _AnsiColorizer docstring.
473
def __init__(self, stream):
474
from win32console import GetStdHandle, STD_OUTPUT_HANDLE, \
475
FOREGROUND_RED, FOREGROUND_BLUE, FOREGROUND_GREEN, \
477
red, green, blue, bold = (FOREGROUND_RED, FOREGROUND_GREEN,
478
FOREGROUND_BLUE, FOREGROUND_INTENSITY)
480
self.screenBuffer = GetStdHandle(STD_OUTPUT_HANDLE)
482
'normal': red | green | blue,
484
'green': green | bold,
486
'yellow': red | green | bold,
487
'magenta': red | blue | bold,
488
'cyan': green | blue | bold,
489
'white': red | green | blue | bold
495
screenBuffer = win32console.GetStdHandle(
496
win32console.STD_OUTPUT_HANDLE)
501
screenBuffer.SetConsoleTextAttribute(
502
win32console.FOREGROUND_RED |
503
win32console.FOREGROUND_GREEN |
504
win32console.FOREGROUND_BLUE)
505
except pywintypes.error:
509
supported = classmethod(supported)
511
def write(self, text, color):
512
color = self._colors[color]
513
self.screenBuffer.SetConsoleTextAttribute(color)
514
self.stream.write(text)
515
self.screenBuffer.SetConsoleTextAttribute(self._colors['normal'])
518
class _NullColorizer(object):
520
See _AnsiColorizer docstring.
522
def __init__(self, stream):
527
supported = classmethod(supported)
529
def write(self, text, color):
530
self.stream.write(text)
533
class TreeReporter(Reporter):
534
"""Print out the tests in the form a tree.
536
Tests are indented according to which class and module they belong.
537
Results are printed in ANSI color.
551
def __init__(self, stream=sys.stdout, tbformat='default', realtime=False):
552
super(TreeReporter, self).__init__(stream, tbformat, realtime)
554
for colorizer in [_Win32Colorizer, _AnsiColorizer, _NullColorizer]:
555
if colorizer.supported():
556
self._colorizer = colorizer(stream)
559
def getDescription(self, test):
561
Return the name of the method which 'test' represents. This is
562
what gets displayed in the leaves of the tree.
564
e.g. getDescription(TestCase('test_foo')) ==> test_foo
566
return test.id().split('.')[-1]
568
def addSuccess(self, test):
569
super(TreeReporter, self).addSuccess(test)
570
self.endLine('[OK]', self.SUCCESS)
572
def addError(self, *args):
573
super(TreeReporter, self).addError(*args)
574
self.endLine('[ERROR]', self.ERROR)
576
def addFailure(self, *args):
577
super(TreeReporter, self).addFailure(*args)
578
self.endLine('[FAIL]', self.FAILURE)
580
def addSkip(self, *args):
581
super(TreeReporter, self).addSkip(*args)
582
self.endLine('[SKIPPED]', self.SKIP)
584
def addExpectedFailure(self, *args):
585
super(TreeReporter, self).addExpectedFailure(*args)
586
self.endLine('[TODO]', self.TODO)
588
def addUnexpectedSuccess(self, *args):
589
super(TreeReporter, self).addUnexpectedSuccess(*args)
590
self.endLine('[SUCCESS!?!]', self.TODONE)
592
def write(self, format, *args):
594
format = format % args
595
self.currentLine = format
596
super(TreeReporter, self).write(self.currentLine)
598
def _testPrelude(self, test):
599
segments = [test.__class__.__module__, test.__class__.__name__]
602
if indentLevel < len(self._lastTest):
603
if seg != self._lastTest[indentLevel]:
604
self.write('%s%s\n' % (self.indent * indentLevel, seg))
606
self.write('%s%s\n' % (self.indent * indentLevel, seg))
608
self._lastTest = segments
610
def cleanupErrors(self, errs):
611
self._colorizer.write(' cleanup errors', self.ERROR)
612
self.endLine('[ERROR]', self.ERROR)
613
super(TreeReporter, self).cleanupErrors(errs)
615
def upDownError(self, method, error, warn, printStatus):
616
self.write(self.color(" %s" % method, self.ERROR))
618
self.endLine('[ERROR]', self.ERROR)
619
super(TreeReporter, self).upDownError(method, error, warn, printStatus)
621
def startTest(self, method):
622
self._testPrelude(method)
623
self.write('%s%s ... ' % (self.indent * (len(self._lastTest)),
624
self.getDescription(method)))
625
super(TreeReporter, self).startTest(method)
627
def endLine(self, message, color):
628
spaces = ' ' * (self.columns - len(self.currentLine) - len(message))
629
super(TreeReporter, self).write(spaces)
630
self._colorizer.write(message, color)
631
super(TreeReporter, self).write("\n")