1<?php 2/* 3 * This file is part of PHPUnit. 4 * 5 * (c) Sebastian Bergmann <sebastian@phpunit.de> 6 * 7 * For the full copyright and license information, please view the LICENSE 8 * file that was distributed with this source code. 9 */ 10 11use SebastianBergmann\Environment\Console; 12 13/** 14 * Prints the result of a TextUI TestRunner run. 15 */ 16class PHPUnit_TextUI_ResultPrinter extends PHPUnit_Util_Printer implements PHPUnit_Framework_TestListener 17{ 18 const EVENT_TEST_START = 0; 19 const EVENT_TEST_END = 1; 20 const EVENT_TESTSUITE_START = 2; 21 const EVENT_TESTSUITE_END = 3; 22 23 const COLOR_NEVER = 'never'; 24 const COLOR_AUTO = 'auto'; 25 const COLOR_ALWAYS = 'always'; 26 const COLOR_DEFAULT = self::COLOR_NEVER; 27 28 /** 29 * @var array 30 */ 31 private static $ansiCodes = [ 32 'bold' => 1, 33 'fg-black' => 30, 34 'fg-red' => 31, 35 'fg-green' => 32, 36 'fg-yellow' => 33, 37 'fg-blue' => 34, 38 'fg-magenta' => 35, 39 'fg-cyan' => 36, 40 'fg-white' => 37, 41 'bg-black' => 40, 42 'bg-red' => 41, 43 'bg-green' => 42, 44 'bg-yellow' => 43, 45 'bg-blue' => 44, 46 'bg-magenta' => 45, 47 'bg-cyan' => 46, 48 'bg-white' => 47 49 ]; 50 51 /** 52 * @var int 53 */ 54 protected $column = 0; 55 56 /** 57 * @var int 58 */ 59 protected $maxColumn; 60 61 /** 62 * @var bool 63 */ 64 protected $lastTestFailed = false; 65 66 /** 67 * @var int 68 */ 69 protected $numAssertions = 0; 70 71 /** 72 * @var int 73 */ 74 protected $numTests = -1; 75 76 /** 77 * @var int 78 */ 79 protected $numTestsRun = 0; 80 81 /** 82 * @var int 83 */ 84 protected $numTestsWidth; 85 86 /** 87 * @var bool 88 */ 89 protected $colors = false; 90 91 /** 92 * @var bool 93 */ 94 protected $debug = false; 95 96 /** 97 * @var bool 98 */ 99 protected $verbose = false; 100 101 /** 102 * @var int 103 */ 104 private $numberOfColumns; 105 106 /** 107 * @var bool 108 */ 109 private $reverse = false; 110 111 /** 112 * @var bool 113 */ 114 private $defectListPrinted = false; 115 116 /** 117 * Constructor. 118 * 119 * @param mixed $out 120 * @param bool $verbose 121 * @param string $colors 122 * @param bool $debug 123 * @param int|string $numberOfColumns 124 * @param bool $reverse 125 * 126 * @throws PHPUnit_Framework_Exception 127 */ 128 public function __construct($out = null, $verbose = false, $colors = self::COLOR_DEFAULT, $debug = false, $numberOfColumns = 80, $reverse = false) 129 { 130 parent::__construct($out); 131 132 if (!is_bool($verbose)) { 133 throw PHPUnit_Util_InvalidArgumentHelper::factory(2, 'boolean'); 134 } 135 136 $availableColors = [self::COLOR_NEVER, self::COLOR_AUTO, self::COLOR_ALWAYS]; 137 138 if (!in_array($colors, $availableColors)) { 139 throw PHPUnit_Util_InvalidArgumentHelper::factory( 140 3, 141 vsprintf('value from "%s", "%s" or "%s"', $availableColors) 142 ); 143 } 144 145 if (!is_bool($debug)) { 146 throw PHPUnit_Util_InvalidArgumentHelper::factory(4, 'boolean'); 147 } 148 149 if (!is_int($numberOfColumns) && $numberOfColumns != 'max') { 150 throw PHPUnit_Util_InvalidArgumentHelper::factory(5, 'integer or "max"'); 151 } 152 153 if (!is_bool($reverse)) { 154 throw PHPUnit_Util_InvalidArgumentHelper::factory(6, 'boolean'); 155 } 156 157 $console = new Console; 158 $maxNumberOfColumns = $console->getNumberOfColumns(); 159 160 if ($numberOfColumns == 'max' || $numberOfColumns > $maxNumberOfColumns) { 161 $numberOfColumns = $maxNumberOfColumns; 162 } 163 164 $this->numberOfColumns = $numberOfColumns; 165 $this->verbose = $verbose; 166 $this->debug = $debug; 167 $this->reverse = $reverse; 168 169 if ($colors === self::COLOR_AUTO && $console->hasColorSupport()) { 170 $this->colors = true; 171 } else { 172 $this->colors = (self::COLOR_ALWAYS === $colors); 173 } 174 } 175 176 /** 177 * @param PHPUnit_Framework_TestResult $result 178 */ 179 public function printResult(PHPUnit_Framework_TestResult $result) 180 { 181 $this->printHeader(); 182 $this->printErrors($result); 183 $this->printWarnings($result); 184 $this->printFailures($result); 185 186 if ($this->verbose) { 187 $this->printRisky($result); 188 $this->printIncompletes($result); 189 $this->printSkipped($result); 190 } 191 192 $this->printFooter($result); 193 } 194 195 /** 196 * @param array $defects 197 * @param string $type 198 */ 199 protected function printDefects(array $defects, $type) 200 { 201 $count = count($defects); 202 203 if ($count == 0) { 204 return; 205 } 206 207 if ($this->defectListPrinted) { 208 $this->write("\n--\n\n"); 209 } 210 211 $this->write( 212 sprintf( 213 "There %s %d %s%s:\n", 214 ($count == 1) ? 'was' : 'were', 215 $count, 216 $type, 217 ($count == 1) ? '' : 's' 218 ) 219 ); 220 221 $i = 1; 222 223 if ($this->reverse) { 224 $defects = array_reverse($defects); 225 } 226 227 foreach ($defects as $defect) { 228 $this->printDefect($defect, $i++); 229 } 230 231 $this->defectListPrinted = true; 232 } 233 234 /** 235 * @param PHPUnit_Framework_TestFailure $defect 236 * @param int $count 237 */ 238 protected function printDefect(PHPUnit_Framework_TestFailure $defect, $count) 239 { 240 $this->printDefectHeader($defect, $count); 241 $this->printDefectTrace($defect); 242 } 243 244 /** 245 * @param PHPUnit_Framework_TestFailure $defect 246 * @param int $count 247 */ 248 protected function printDefectHeader(PHPUnit_Framework_TestFailure $defect, $count) 249 { 250 $this->write( 251 sprintf( 252 "\n%d) %s\n", 253 $count, 254 $defect->getTestName() 255 ) 256 ); 257 } 258 259 /** 260 * @param PHPUnit_Framework_TestFailure $defect 261 */ 262 protected function printDefectTrace(PHPUnit_Framework_TestFailure $defect) 263 { 264 $e = $defect->thrownException(); 265 $this->write((string) $e); 266 267 while ($e = $e->getPrevious()) { 268 $this->write("\nCaused by\n" . $e); 269 } 270 } 271 272 /** 273 * @param PHPUnit_Framework_TestResult $result 274 */ 275 protected function printErrors(PHPUnit_Framework_TestResult $result) 276 { 277 $this->printDefects($result->errors(), 'error'); 278 } 279 280 /** 281 * @param PHPUnit_Framework_TestResult $result 282 */ 283 protected function printFailures(PHPUnit_Framework_TestResult $result) 284 { 285 $this->printDefects($result->failures(), 'failure'); 286 } 287 288 /** 289 * @param PHPUnit_Framework_TestResult $result 290 */ 291 protected function printWarnings(PHPUnit_Framework_TestResult $result) 292 { 293 $this->printDefects($result->warnings(), 'warning'); 294 } 295 296 /** 297 * @param PHPUnit_Framework_TestResult $result 298 */ 299 protected function printIncompletes(PHPUnit_Framework_TestResult $result) 300 { 301 $this->printDefects($result->notImplemented(), 'incomplete test'); 302 } 303 304 /** 305 * @param PHPUnit_Framework_TestResult $result 306 */ 307 protected function printRisky(PHPUnit_Framework_TestResult $result) 308 { 309 $this->printDefects($result->risky(), 'risky test'); 310 } 311 312 /** 313 * @param PHPUnit_Framework_TestResult $result 314 */ 315 protected function printSkipped(PHPUnit_Framework_TestResult $result) 316 { 317 $this->printDefects($result->skipped(), 'skipped test'); 318 } 319 320 protected function printHeader() 321 { 322 $this->write("\n\n" . PHP_Timer::resourceUsage() . "\n\n"); 323 } 324 325 /** 326 * @param PHPUnit_Framework_TestResult $result 327 */ 328 protected function printFooter(PHPUnit_Framework_TestResult $result) 329 { 330 if (count($result) === 0) { 331 $this->writeWithColor( 332 'fg-black, bg-yellow', 333 'No tests executed!' 334 ); 335 336 return; 337 } 338 339 if ($result->wasSuccessful() && 340 $result->allHarmless() && 341 $result->allCompletelyImplemented() && 342 $result->noneSkipped()) { 343 $this->writeWithColor( 344 'fg-black, bg-green', 345 sprintf( 346 'OK (%d test%s, %d assertion%s)', 347 count($result), 348 (count($result) == 1) ? '' : 's', 349 $this->numAssertions, 350 ($this->numAssertions == 1) ? '' : 's' 351 ) 352 ); 353 } else { 354 if ($result->wasSuccessful()) { 355 $color = 'fg-black, bg-yellow'; 356 357 if ($this->verbose) { 358 $this->write("\n"); 359 } 360 361 $this->writeWithColor( 362 $color, 363 'OK, but incomplete, skipped, or risky tests!' 364 ); 365 } else { 366 $this->write("\n"); 367 368 if ($result->errorCount()) { 369 $color = 'fg-white, bg-red'; 370 371 $this->writeWithColor( 372 $color, 373 'ERRORS!' 374 ); 375 } elseif ($result->failureCount()) { 376 $color = 'fg-white, bg-red'; 377 378 $this->writeWithColor( 379 $color, 380 'FAILURES!' 381 ); 382 } elseif ($result->warningCount()) { 383 $color = 'fg-black, bg-yellow'; 384 385 $this->writeWithColor( 386 $color, 387 'WARNINGS!' 388 ); 389 } 390 } 391 392 $this->writeCountString(count($result), 'Tests', $color, true); 393 $this->writeCountString($this->numAssertions, 'Assertions', $color, true); 394 $this->writeCountString($result->errorCount(), 'Errors', $color); 395 $this->writeCountString($result->failureCount(), 'Failures', $color); 396 $this->writeCountString($result->warningCount(), 'Warnings', $color); 397 $this->writeCountString($result->skippedCount(), 'Skipped', $color); 398 $this->writeCountString($result->notImplementedCount(), 'Incomplete', $color); 399 $this->writeCountString($result->riskyCount(), 'Risky', $color); 400 $this->writeWithColor($color, '.', true); 401 } 402 } 403 404 public function printWaitPrompt() 405 { 406 $this->write("\n<RETURN> to continue\n"); 407 } 408 409 /** 410 * An error occurred. 411 * 412 * @param PHPUnit_Framework_Test $test 413 * @param Exception $e 414 * @param float $time 415 */ 416 public function addError(PHPUnit_Framework_Test $test, Exception $e, $time) 417 { 418 $this->writeProgressWithColor('fg-red, bold', 'E'); 419 $this->lastTestFailed = true; 420 } 421 422 /** 423 * A failure occurred. 424 * 425 * @param PHPUnit_Framework_Test $test 426 * @param PHPUnit_Framework_AssertionFailedError $e 427 * @param float $time 428 */ 429 public function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) 430 { 431 $this->writeProgressWithColor('bg-red, fg-white', 'F'); 432 $this->lastTestFailed = true; 433 } 434 435 /** 436 * A warning occurred. 437 * 438 * @param PHPUnit_Framework_Test $test 439 * @param PHPUnit_Framework_Warning $e 440 * @param float $time 441 */ 442 public function addWarning(PHPUnit_Framework_Test $test, PHPUnit_Framework_Warning $e, $time) 443 { 444 $this->writeProgressWithColor('fg-yellow, bold', 'W'); 445 $this->lastTestFailed = true; 446 } 447 448 /** 449 * Incomplete test. 450 * 451 * @param PHPUnit_Framework_Test $test 452 * @param Exception $e 453 * @param float $time 454 */ 455 public function addIncompleteTest(PHPUnit_Framework_Test $test, Exception $e, $time) 456 { 457 $this->writeProgressWithColor('fg-yellow, bold', 'I'); 458 $this->lastTestFailed = true; 459 } 460 461 /** 462 * Risky test. 463 * 464 * @param PHPUnit_Framework_Test $test 465 * @param Exception $e 466 * @param float $time 467 */ 468 public function addRiskyTest(PHPUnit_Framework_Test $test, Exception $e, $time) 469 { 470 $this->writeProgressWithColor('fg-yellow, bold', 'R'); 471 $this->lastTestFailed = true; 472 } 473 474 /** 475 * Skipped test. 476 * 477 * @param PHPUnit_Framework_Test $test 478 * @param Exception $e 479 * @param float $time 480 */ 481 public function addSkippedTest(PHPUnit_Framework_Test $test, Exception $e, $time) 482 { 483 $this->writeProgressWithColor('fg-cyan, bold', 'S'); 484 $this->lastTestFailed = true; 485 } 486 487 /** 488 * A testsuite started. 489 * 490 * @param PHPUnit_Framework_TestSuite $suite 491 */ 492 public function startTestSuite(PHPUnit_Framework_TestSuite $suite) 493 { 494 if ($this->numTests == -1) { 495 $this->numTests = count($suite); 496 $this->numTestsWidth = strlen((string) $this->numTests); 497 $this->maxColumn = $this->numberOfColumns - strlen(' / (XXX%)') - (2 * $this->numTestsWidth); 498 } 499 } 500 501 /** 502 * A testsuite ended. 503 * 504 * @param PHPUnit_Framework_TestSuite $suite 505 */ 506 public function endTestSuite(PHPUnit_Framework_TestSuite $suite) 507 { 508 } 509 510 /** 511 * A test started. 512 * 513 * @param PHPUnit_Framework_Test $test 514 */ 515 public function startTest(PHPUnit_Framework_Test $test) 516 { 517 if ($this->debug) { 518 $this->write( 519 sprintf( 520 "\nStarting test '%s'.\n", 521 PHPUnit_Util_Test::describe($test) 522 ) 523 ); 524 } 525 } 526 527 /** 528 * A test ended. 529 * 530 * @param PHPUnit_Framework_Test $test 531 * @param float $time 532 */ 533 public function endTest(PHPUnit_Framework_Test $test, $time) 534 { 535 if (!$this->lastTestFailed) { 536 $this->writeProgress('.'); 537 } 538 539 if ($test instanceof PHPUnit_Framework_TestCase) { 540 $this->numAssertions += $test->getNumAssertions(); 541 } elseif ($test instanceof PHPUnit_Extensions_PhptTestCase) { 542 $this->numAssertions++; 543 } 544 545 $this->lastTestFailed = false; 546 547 if ($test instanceof PHPUnit_Framework_TestCase) { 548 if (!$test->hasExpectationOnOutput()) { 549 $this->write($test->getActualOutput()); 550 } 551 } 552 } 553 554 /** 555 * @param string $progress 556 */ 557 protected function writeProgress($progress) 558 { 559 $this->write($progress); 560 $this->column++; 561 $this->numTestsRun++; 562 563 if ($this->column == $this->maxColumn 564 || $this->numTestsRun == $this->numTests 565 ) { 566 if ($this->numTestsRun == $this->numTests) { 567 $this->write(str_repeat(' ', $this->maxColumn - $this->column)); 568 } 569 570 $this->write( 571 sprintf( 572 ' %' . $this->numTestsWidth . 'd / %' . 573 $this->numTestsWidth . 'd (%3s%%)', 574 $this->numTestsRun, 575 $this->numTests, 576 floor(($this->numTestsRun / $this->numTests) * 100) 577 ) 578 ); 579 580 if ($this->column == $this->maxColumn) { 581 $this->writeNewLine(); 582 } 583 } 584 } 585 586 protected function writeNewLine() 587 { 588 $this->column = 0; 589 $this->write("\n"); 590 } 591 592 /** 593 * Formats a buffer with a specified ANSI color sequence if colors are 594 * enabled. 595 * 596 * @param string $color 597 * @param string $buffer 598 * 599 * @return string 600 */ 601 protected function formatWithColor($color, $buffer) 602 { 603 if (!$this->colors) { 604 return $buffer; 605 } 606 607 $codes = array_map('trim', explode(',', $color)); 608 $lines = explode("\n", $buffer); 609 $padding = max(array_map('strlen', $lines)); 610 $styles = []; 611 612 foreach ($codes as $code) { 613 $styles[] = self::$ansiCodes[$code]; 614 } 615 616 $style = sprintf("\x1b[%sm", implode(';', $styles)); 617 618 $styledLines = []; 619 620 foreach ($lines as $line) { 621 $styledLines[] = $style . str_pad($line, $padding) . "\x1b[0m"; 622 } 623 624 return implode("\n", $styledLines); 625 } 626 627 /** 628 * Writes a buffer out with a color sequence if colors are enabled. 629 * 630 * @param string $color 631 * @param string $buffer 632 * @param bool $lf 633 */ 634 protected function writeWithColor($color, $buffer, $lf = true) 635 { 636 $this->write($this->formatWithColor($color, $buffer)); 637 638 if ($lf) { 639 $this->write("\n"); 640 } 641 } 642 643 /** 644 * Writes progress with a color sequence if colors are enabled. 645 * 646 * @param string $color 647 * @param string $buffer 648 */ 649 protected function writeProgressWithColor($color, $buffer) 650 { 651 $buffer = $this->formatWithColor($color, $buffer); 652 $this->writeProgress($buffer); 653 } 654 655 /** 656 * @param int $count 657 * @param string $name 658 * @param string $color 659 * @param bool $always 660 */ 661 private function writeCountString($count, $name, $color, $always = false) 662 { 663 static $first = true; 664 665 if ($always || $count > 0) { 666 $this->writeWithColor( 667 $color, 668 sprintf( 669 '%s%s: %d', 670 !$first ? ', ' : '', 671 $name, 672 $count 673 ), 674 false 675 ); 676 677 $first = false; 678 } 679 } 680} 681