Completed
Push — short-traces ( e2d26e )
by Erin
01:50
created

AbstractBaseReporter::outputTrace()   B

Complexity

Conditions 5
Paths 7

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 5
eloc 14
nc 7
nop 1
dl 0
loc 23
rs 8.5906
c 0
b 0
f 0
1
<?php
2
namespace Peridot\Reporter;
3
4
use Evenement\EventEmitterInterface;
5
use Peridot\Configuration;
6
use Peridot\Core\HasEventEmitterTrait;
7
use Peridot\Core\Test;
8
use Peridot\Core\TestInterface;
9
use Symfony\Component\Console\Output\OutputInterface;
10
use Symfony\Component\Console\Output\StreamOutput;
11
12
/**
13
 * The base class for all Peridot reporters. Sits on top of an OutputInterface
14
 * and an EventEmitter in order to report Peridot results.
15
 *
16
 * @package Peridot\Reporter
17
 */
18
abstract class AbstractBaseReporter implements ReporterInterface
19
{
20
    use HasEventEmitterTrait;
21
22
    /**
23
     * @var \Peridot\Configuration
24
     */
25
    protected $configuration;
26
27
    /**
28
     * @var \Symfony\Component\Console\Output\OutputInterface
29
     */
30
    protected $output;
31
32
    /**
33
     * @var array
34
     */
35
    protected $errors = [];
36
37
    /**
38
     * @var int
39
     */
40
    protected $passing = 0;
41
42
    /**
43
     * @var int
44
     */
45
    protected $pending = 0;
46
47
    /**
48
     * @var double|integer
49
     */
50
    protected $time;
51
52
    /**
53
     * Maps color names to left and right color sequences.
54
     *
55
     * @var array
56
     */
57
    protected $colors = array(
58
        'white' => ['left' => "\033[37m", 'right' => "\033[39m"],
59
        'success' => ['left' => "\033[32m", 'right' => "\033[39m"],
60
        'error' => ['left' => "\033[31m", 'right' => "\033[39m"],
61
        'muted' => ['left' => "\033[90m", 'right' => "\033[0m"],
62
        'pending' => ['left' => "\033[36m", 'right' => "\033[39m"],
63
    );
64
65
    /**
66
     * Maps symbol names to symbols
67
     *
68
     * @var array
69
     */
70
    protected $symbols = array(
71
        'check' => '✓'
72
    );
73
74
    /**
75
     * @param Configuration $configuration
76
     * @param OutputInterface $output
77
     * @param EventEmitterInterface $eventEmitter
78
     */
79
    public function __construct(
80
        Configuration $configuration,
81
        OutputInterface $output,
82
        EventEmitterInterface $eventEmitter
83
    ) {
84
        $this->configuration = $configuration;
85
        $this->output = $output;
86
        $this->eventEmitter = $eventEmitter;
87
88
        $this->registerSymbols();
89
90
        $this->registerEvents();
91
92
        $this->init();
93
    }
94
95
    /**
96
     * Given a color name, colorize the provided text in that
97
     * color
98
     *
99
     * @param $key
100
     * @param $text
101
     * @return string
102
     */
103
    public function color($key, $text)
104
    {
105
        $colorsEnabled = $this->configuration->areColorsEnabled() && $this->hasColorSupport();
106
        $colorsEnabledExplicit = $this->configuration->areColorsEnabledExplicit();
107
108
        if (!$colorsEnabled && !$colorsEnabledExplicit) {
109
            return $text;
110
        }
111
112
        $color = $this->colors[$key];
113
114
        return sprintf("%s%s%s", $color['left'], $text, $color['right']);
115
    }
116
117
    /**
118
     * Fetch a symbol by name
119
     *
120
     * @param $name
121
     * @return string
122
     */
123
    public function symbol($name)
124
    {
125
        return $this->symbols[$name];
126
    }
127
128
    /**
129
     * Return the OutputInterface associated with the Reporter
130
     *
131
     * @return \Symfony\Component\Console\Output\OutputInterface
132
     */
133
    public function getOutput()
134
    {
135
        return $this->output;
136
    }
137
138
    /**
139
     * Return the Configuration associated with the Reporter
140
     *
141
     * @return \Peridot\Configuration
142
     */
143
    public function getConfiguration()
144
    {
145
        return $this->configuration;
146
    }
147
148
    /**
149
     * Set the run time to report.
150
     *
151
     * @param float $time
152
     */
153
    public function setTime($time)
154
    {
155
        $this->time = $time;
156
    }
157
158
    /**
159
     * Get the run time to report.
160
     *
161
     * @return float
162
     */
163
    public function getTime()
164
    {
165
        return $this->time;
166
    }
167
168
    /**
169
     * Output result footer
170
     */
171
    public function footer()
172
    {
173
        $this->output->write($this->color('success', sprintf("\n  %d passing", $this->passing)));
174
        $this->output->writeln(sprintf($this->color('muted', " (%s)"), \PHP_Timer::secondsToTimeString($this->getTime())));
175
        if (! empty($this->errors)) {
176
            $this->output->writeln($this->color('error', sprintf("  %d failing", count($this->errors))));
177
        }
178
        if ($this->pending) {
179
            $this->output->writeln($this->color('pending', sprintf("  %d pending", $this->pending)));
180
        }
181
        $this->output->writeln("");
182
        $errorCount = count($this->errors);
183
        for ($i = 0; $i < $errorCount; $i++) {
184
            list($test, $error) = $this->errors[$i];
185
            $this->outputError($i + 1, $test, $error);
186
        }
187
    }
188
189
    /**
190
     * Output a test failure.
191
     *
192
     * @param int $errorNumber
193
     * @param TestInterface $test
194
     * @param $exception - an exception like interface with ->getMessage(), ->getTraceAsString()
195
     */
196
    protected function outputError($errorNumber, TestInterface $test, $exception)
197
    {
198
        $this->output->writeln(sprintf("  %d)%s:", $errorNumber, $test->getTitle()));
199
200
        $message = sprintf("     %s", str_replace(PHP_EOL, PHP_EOL . "     ", $exception->getMessage()));
201
        $this->output->writeln($this->color('error', $message));
202
203
        $location = sprintf('     at %s:%d', $exception->getFile(), $exception->getLine());
204
        $this->output->writeln($location);
205
206
        $this->outputTrace($exception->getTrace());
207
    }
208
209
    /**
210
     * Output a stack trace.
211
     *
212
     * @param array $trace
213
     */
214
    protected function outputTrace(array $trace)
215
    {
216
        foreach ($trace as $index => $entry) {
217
            if (isset($entry['class'])) {
218
                $function = $entry['class'] . $entry['type'] . $entry['function'];
219
            } else {
220
                $function = $entry['function'];
221
            }
222
223
            if (strncmp($function, 'Peridot\\', 8) === 0) {
224
                break;
225
            }
226
227
            if (isset($entry['file'])) {
228
                $location = sprintf(' (%s:%d)', $entry['file'], $entry['line']);
229
            } else {
230
                $location = '';
231
            }
232
233
            $entry = sprintf('       #%d %s%s', $index, $function, $location);
234
            $this->output->writeln($this->color('muted', $entry));
235
        }
236
    }
237
238
    /**
239
     * Determine if colorized output is supported by the reporters output.
240
     * Taken from Symfony's console output with some slight modifications
241
     * to use the reporter's output stream
242
     *
243
     * @return bool
244
     */
245
    protected function hasColorSupport()
246
    {
247
        if ($this->isOnWindows()) {
248
            return $this->hasAnsiSupport();
249
        }
250
251
        return $this->hasTty();
252
    }
253
254
    /**
255
     * Register reporter symbols, additionally checking OS compatibility.
256
     */
257
    protected function registerSymbols()
258
    {
259
        //update symbols for windows
260
        if ($this->isOnWindows()) {
261
            $this->symbols['check'] = chr(251);
262
        }
263
    }
264
265
    /**
266
     * Return true if reporter is being used on windows
267
     *
268
     * @return bool
269
     */
270
    protected function isOnWindows()
271
    {
272
        return DIRECTORY_SEPARATOR == '\\';
273
    }
274
275
    /**
276
     * Register events tracking state relevant to all reporters.
277
     */
278
    private function registerEvents()
279
    {
280
        $this->eventEmitter->on('runner.end', [$this, 'setTime']);
281
282
        $this->eventEmitter->on('test.failed', function (Test $test, $e) {
283
            $this->errors[] = [$test, $e];
284
        });
285
286
        $this->eventEmitter->on('test.passed', function () {
287
            $this->passing++;
288
        });
289
290
        $this->eventEmitter->on('test.pending', function () {
291
            $this->pending++;
292
        });
293
    }
294
295
    /**
296
     * Determine if the terminal has ansicon support
297
     *
298
     * @return bool
299
     */
300
    private function hasAnsiSupport()
301
    {
302
        return false !== getenv('ANSICON') || 'ON' === getenv('ConEmuANSI');
303
    }
304
305
    /**
306
     * Determine if reporter is reporting to a tty terminal.
307
     *
308
     * @return bool
309
     */
310
    private function hasTty()
311
    {
312
        if (! $this->output instanceof StreamOutput) {
313
            return false;
314
        }
315
316
        if (getenv("PERIDOT_TTY")) {
317
            return true;
318
        }
319
320
        return $this->isTtyTerminal($this->output);
321
    }
322
323
    /**
324
     * See if stream output is a tty terminal.
325
     *
326
     * @return bool
327
     */
328
    private function isTtyTerminal(StreamOutput $output)
329
    {
330
        $tty = function_exists('posix_isatty') && @posix_isatty($output->getStream());
331
        if ($tty) {
332
            putenv("PERIDOT_TTY=1");
333
        }
334
        return $tty;
335
    }
336
337
    private function renderTrace($exception)
0 ignored issues
show
Unused Code introduced by
This method is not used, and could be removed.
Loading history...
338
    {
339
        $trace = $exception->getTrace();
340
        $rendered = '';
341
342
        foreach ($trace as $index => $entry) {
343
            if ($this->isPeridotTraceEntry($entry)) {
344
                break;
345
            }
346
347
            $rendered .= '       #' . $index . ' ';
348
349
            if (isset($entry['class'])) {
350
                $parts = explode('\\', $entry['class']);
351
                $rendered .= $indent . array_pop($parts) . $entry['type'] . $entry['function'];
0 ignored issues
show
Bug introduced by
The variable $indent does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
352
            } else {
353
                $parts = explode('\\', $entry['function']);
354
                $rendered .= array_pop($parts);
355
            }
356
357
            if (isset($entry['file'])) {
358
                $rendered .= ' (' . $entry['file'] . ':' . $entry['line'] . ')';
359
            }
360
361
            $rendered .= PHP_EOL;
362
        }
363
364
        return $rendered;
365
    }
366
367
368
    private function isPeridotTraceEntry($entry)
369
    {
370
        $prefix = 'Peridot\\';
371
372
        if (isset($entry['class'])) {
373
            return 0 === strpos($entry['class'], $prefix);
374
        }
375
376
        return 0 === strpos($entry['function'], $prefix);
377
    }
378
379
    /**
380
     * Initialize reporter. Setup and listen for events
381
     *
382
     * @return void
383
     */
384
    abstract public function init();
385
}
386