| 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 |