Subversion-Projekte lars-tiefland.laravel_shop

Revision

Revision 150 | Zur aktuellen Revision | Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
148 lars 1
<?php declare(strict_types=1);
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
namespace PHPUnit\Util\TestDox;
11
 
12
use const PHP_EOL;
13
use function array_map;
14
use function ceil;
15
use function count;
16
use function explode;
17
use function get_class;
18
use function implode;
19
use function preg_match;
20
use function sprintf;
21
use function strlen;
22
use function strpos;
23
use function trim;
24
use PHPUnit\Framework\Test;
25
use PHPUnit\Framework\TestCase;
26
use PHPUnit\Framework\TestResult;
27
use PHPUnit\Runner\BaseTestRunner;
28
use PHPUnit\Runner\PhptTestCase;
29
use PHPUnit\Util\Color;
30
use SebastianBergmann\Timer\ResourceUsageFormatter;
31
use SebastianBergmann\Timer\Timer;
32
use Throwable;
33
 
34
/**
35
 * @internal This class is not covered by the backward compatibility promise for PHPUnit
36
 */
37
class CliTestDoxPrinter extends TestDoxPrinter
38
{
39
    /**
40
     * The default Testdox left margin for messages is a vertical line.
41
     */
42
    private const PREFIX_SIMPLE = [
43
        'default' => '│',
44
        'start'   => '│',
45
        'message' => '│',
46
        'diff'    => '│',
47
        'trace'   => '│',
48
        'last'    => '│',
49
    ];
50
 
51
    /**
52
     * Colored Testdox use box-drawing for a more textured map of the message.
53
     */
54
    private const PREFIX_DECORATED = [
55
        'default' => '│',
56
        'start'   => '┐',
57
        'message' => '├',
58
        'diff'    => '┊',
59
        'trace'   => '╵',
60
        'last'    => 'â”´',
61
    ];
62
 
63
    private const SPINNER_ICONS = [
64
        " \e[36m◐\e[0m running tests",
65
        " \e[36mâ—“\e[0m running tests",
66
        " \e[36mâ—‘\e[0m running tests",
67
        " \e[36mâ—’\e[0m running tests",
68
    ];
69
 
70
    private const STATUS_STYLES = [
71
        BaseTestRunner::STATUS_PASSED => [
72
            'symbol' => '✔',
73
            'color'  => 'fg-green',
74
        ],
75
        BaseTestRunner::STATUS_ERROR => [
76
            'symbol'  => '✘',
77
            'color'   => 'fg-yellow',
78
            'message' => 'bg-yellow,fg-black',
79
        ],
80
        BaseTestRunner::STATUS_FAILURE => [
81
            'symbol'  => '✘',
82
            'color'   => 'fg-red',
83
            'message' => 'bg-red,fg-white',
84
        ],
85
        BaseTestRunner::STATUS_SKIPPED => [
86
            'symbol'  => '↩',
87
            'color'   => 'fg-cyan',
88
            'message' => 'fg-cyan',
89
        ],
90
        BaseTestRunner::STATUS_RISKY => [
91
            'symbol'  => '☢',
92
            'color'   => 'fg-yellow',
93
            'message' => 'fg-yellow',
94
        ],
95
        BaseTestRunner::STATUS_INCOMPLETE => [
96
            'symbol'  => '∅',
97
            'color'   => 'fg-yellow',
98
            'message' => 'fg-yellow',
99
        ],
100
        BaseTestRunner::STATUS_WARNING => [
101
            'symbol'  => '⚠',
102
            'color'   => 'fg-yellow',
103
            'message' => 'fg-yellow',
104
        ],
105
        BaseTestRunner::STATUS_UNKNOWN => [
106
            'symbol'  => '?',
107
            'color'   => 'fg-blue',
108
            'message' => 'fg-white,bg-blue',
109
        ],
110
    ];
111
 
112
    /**
113
     * @var int[]
114
     */
115
    private $nonSuccessfulTestResults = [];
116
 
117
    /**
118
     * @var Timer
119
     */
120
    private $timer;
121
 
122
    /**
123
     * @param null|resource|string $out
124
     * @param int|string           $numberOfColumns
125
     *
126
     * @throws \PHPUnit\Framework\Exception
127
     */
128
    public function __construct($out = null, bool $verbose = false, string $colors = self::COLOR_DEFAULT, bool $debug = false, $numberOfColumns = 80, bool $reverse = false)
129
    {
130
        parent::__construct($out, $verbose, $colors, $debug, $numberOfColumns, $reverse);
131
 
132
        $this->timer = new Timer;
133
 
134
        $this->timer->start();
135
    }
136
 
137
    public function printResult(TestResult $result): void
138
    {
139
        $this->printHeader($result);
140
 
141
        $this->printNonSuccessfulTestsSummary($result->count());
142
 
143
        $this->printFooter($result);
144
    }
145
 
146
    protected function printHeader(TestResult $result): void
147
    {
148
        $this->write("\n" . (new ResourceUsageFormatter)->resourceUsage($this->timer->stop()) . "\n\n");
149
    }
150
 
151
    protected function formatClassName(Test $test): string
152
    {
153
        if ($test instanceof TestCase) {
154
            return $this->prettifier->prettifyTestClass(get_class($test));
155
        }
156
 
157
        return get_class($test);
158
    }
159
 
160
    /**
161
     * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
162
     */
163
    protected function registerTestResult(Test $test, ?Throwable $t, int $status, float $time, bool $verbose): void
164
    {
165
        if ($status !== BaseTestRunner::STATUS_PASSED) {
166
            $this->nonSuccessfulTestResults[] = $this->testIndex;
167
        }
168
 
169
        parent::registerTestResult($test, $t, $status, $time, $verbose);
170
    }
171
 
172
    /**
173
     * @throws \SebastianBergmann\RecursionContext\InvalidArgumentException
174
     */
175
    protected function formatTestName(Test $test): string
176
    {
177
        if ($test instanceof TestCase) {
178
            return $this->prettifier->prettifyTestCase($test);
179
        }
180
 
181
        return parent::formatTestName($test);
182
    }
183
 
184
    protected function writeTestResult(array $prevResult, array $result): void
185
    {
186
        // spacer line for new suite headers and after verbose messages
187
        if ($prevResult['testName'] !== '' &&
188
            (!empty($prevResult['message']) || $prevResult['className'] !== $result['className'])) {
189
            $this->write(PHP_EOL);
190
        }
191
 
192
        // suite header
193
        if ($prevResult['className'] !== $result['className']) {
194
            $this->write($this->colorizeTextBox('underlined', $result['className']) . PHP_EOL);
195
        }
196
 
197
        // test result line
198
        if ($this->colors && $result['className'] === PhptTestCase::class) {
199
            $testName = Color::colorizePath($result['testName'], $prevResult['testName'], true);
200
        } else {
201
            $testName = $result['testMethod'];
202
        }
203
 
204
        $style = self::STATUS_STYLES[$result['status']];
205
        $line  = sprintf(
206
            ' %s %s%s' . PHP_EOL,
207
            $this->colorizeTextBox($style['color'], $style['symbol']),
208
            $testName,
209
            $this->verbose ? ' ' . $this->formatRuntime($result['time'], $style['color']) : ''
210
        );
211
 
212
        $this->write($line);
213
 
214
        // additional information when verbose
215
        $this->write($result['message']);
216
    }
217
 
218
    protected function formatThrowable(Throwable $t, ?int $status = null): string
219
    {
220
        return trim(\PHPUnit\Framework\TestFailure::exceptionToString($t));
221
    }
222
 
223
    protected function colorizeMessageAndDiff(string $style, string $buffer): array
224
    {
225
        $lines      = $buffer ? array_map('\rtrim', explode(PHP_EOL, $buffer)) : [];
226
        $message    = [];
227
        $diff       = [];
228
        $insideDiff = false;
229
 
230
        foreach ($lines as $line) {
231
            if ($line === '--- Expected') {
232
                $insideDiff = true;
233
            }
234
 
235
            if (!$insideDiff) {
236
                $message[] = $line;
237
            } else {
238
                if (strpos($line, '-') === 0) {
239
                    $line = Color::colorize('fg-red', Color::visualizeWhitespace($line, true));
240
                } elseif (strpos($line, '+') === 0) {
241
                    $line = Color::colorize('fg-green', Color::visualizeWhitespace($line, true));
242
                } elseif ($line === '@@ @@') {
243
                    $line = Color::colorize('fg-cyan', $line);
244
                }
245
                $diff[] = $line;
246
            }
247
        }
248
        $diff = implode(PHP_EOL, $diff);
249
 
250
        if (!empty($message)) {
251
            $message = $this->colorizeTextBox($style, implode(PHP_EOL, $message));
252
        }
253
 
254
        return [$message, $diff];
255
    }
256
 
257
    protected function formatStacktrace(Throwable $t): string
258
    {
259
        $trace = \PHPUnit\Util\Filter::getFilteredStacktrace($t);
260
 
261
        if (!$this->colors) {
262
            return $trace;
263
        }
264
 
265
        $lines    = [];
266
        $prevPath = '';
267
 
268
        foreach (explode(PHP_EOL, $trace) as $line) {
269
            if (preg_match('/^(.*):(\d+)$/', $line, $matches)) {
270
                $lines[] = Color::colorizePath($matches[1], $prevPath) .
271
                    Color::dim(':') .
272
                    Color::colorize('fg-blue', $matches[2]) .
273
                    "\n";
274
                $prevPath = $matches[1];
275
            } else {
276
                $lines[]  = $line;
277
                $prevPath = '';
278
            }
279
        }
280
 
281
        return implode('', $lines);
282
    }
283
 
284
    protected function formatTestResultMessage(Throwable $t, array $result, ?string $prefix = null): string
285
    {
286
        $message = $this->formatThrowable($t, $result['status']);
287
        $diff    = '';
288
 
289
        if (!($this->verbose || $result['verbose'])) {
290
            return '';
291
        }
292
 
293
        if ($message && $this->colors) {
294
            $style            = self::STATUS_STYLES[$result['status']]['message'] ?? '';
295
            [$message, $diff] = $this->colorizeMessageAndDiff($style, $message);
296
        }
297
 
298
        if ($prefix === null || !$this->colors) {
299
            $prefix = self::PREFIX_SIMPLE;
300
        }
301
 
302
        if ($this->colors) {
303
            $color  = self::STATUS_STYLES[$result['status']]['color'] ?? '';
304
            $prefix = array_map(static function ($p) use ($color)
305
            {
306
                return Color::colorize($color, $p);
307
            }, self::PREFIX_DECORATED);
308
        }
309
 
310
        $trace = $this->formatStacktrace($t);
311
        $out   = $this->prefixLines($prefix['start'], PHP_EOL) . PHP_EOL;
312
 
313
        if ($message) {
314
            $out .= $this->prefixLines($prefix['message'], $message . PHP_EOL) . PHP_EOL;
315
        }
316
 
317
        if ($diff) {
318
            $out .= $this->prefixLines($prefix['diff'], $diff . PHP_EOL) . PHP_EOL;
319
        }
320
 
321
        if ($trace) {
322
            if ($message || $diff) {
323
                $out .= $this->prefixLines($prefix['default'], PHP_EOL) . PHP_EOL;
324
            }
325
            $out .= $this->prefixLines($prefix['trace'], $trace . PHP_EOL) . PHP_EOL;
326
        }
327
        $out .= $this->prefixLines($prefix['last'], PHP_EOL) . PHP_EOL;
328
 
329
        return $out;
330
    }
331
 
332
    protected function drawSpinner(): void
333
    {
334
        if ($this->colors) {
335
            $id = $this->spinState % count(self::SPINNER_ICONS);
336
            $this->write(self::SPINNER_ICONS[$id]);
337
        }
338
    }
339
 
340
    protected function undrawSpinner(): void
341
    {
342
        if ($this->colors) {
343
            $id = $this->spinState % count(self::SPINNER_ICONS);
344
            $this->write("\e[1K\e[" . strlen(self::SPINNER_ICONS[$id]) . 'D');
345
        }
346
    }
347
 
348
    private function formatRuntime(float $time, string $color = ''): string
349
    {
350
        if (!$this->colors) {
351
            return sprintf('[%.2f ms]', $time * 1000);
352
        }
353
 
354
        if ($time > 1) {
355
            $color = 'fg-magenta';
356
        }
357
 
358
        return Color::colorize($color, ' ' . (int) ceil($time * 1000) . ' ' . Color::dim('ms'));
359
    }
360
 
361
    private function printNonSuccessfulTestsSummary(int $numberOfExecutedTests): void
362
    {
363
        if (empty($this->nonSuccessfulTestResults)) {
364
            return;
365
        }
366
 
367
        if ((count($this->nonSuccessfulTestResults) / $numberOfExecutedTests) >= 0.7) {
368
            return;
369
        }
370
 
371
        $this->write("Summary of non-successful tests:\n\n");
372
 
373
        $prevResult = $this->getEmptyTestResult();
374
 
375
        foreach ($this->nonSuccessfulTestResults as $testIndex) {
376
            $result = $this->testResults[$testIndex];
377
            $this->writeTestResult($prevResult, $result);
378
            $prevResult = $result;
379
        }
380
    }
381
}