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