CodeCoverageListener   A
last analyzed

Complexity

Total Complexity 26

Size/Duplication

Total Lines 201
Duplicated Lines 0 %

Test Coverage

Coverage 60%

Importance

Changes 10
Bugs 0 Features 0
Metric Value
wmc 26
eloc 86
c 10
b 0
f 0
dl 0
loc 201
ccs 57
cts 95
cp 0.6
rs 10

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 15 1
A beforeExample() 0 20 3
A afterExample() 0 7 2
B afterSuite() 0 25 7
A setOptions() 0 3 1
A filterDirectoryParams() 0 21 4
A getSubscribedEvents() 0 7 1
B beforeSuite() 0 41 7
1
<?php
2
3
/**
4
 * This file is part of the friends-of-phpspec/phpspec-code-coverage package.
5
 *
6
 * @author  ek9 <[email protected]>
7
 * @license MIT
8
 *
9
 * For the full copyright and license information, please see the LICENSE file
10
 * that was distributed with this source code.
11
 */
12
13
declare(strict_types=1);
14
15
namespace FriendsOfPhpSpec\PhpSpec\CodeCoverage\Listener;
16
17
use FriendsOfPhpSpec\PhpSpec\CodeCoverage\Exception\ConfigurationException;
18
use PhpSpec\Console\ConsoleIO;
19
use PhpSpec\Event\ExampleEvent;
20
use PhpSpec\Event\SuiteEvent;
21
use SebastianBergmann\CodeCoverage\CodeCoverage;
22
use SebastianBergmann\CodeCoverage\Report;
23
use SebastianBergmann\FileIterator\Facade as FileIteratorFacade;
24
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
25
26
use function gettype;
27
use function is_array;
28
use function is_string;
29
30
/**
31
 * @author Henrik Bjornskov
32
 */
33
class CodeCoverageListener implements EventSubscriberInterface
34
{
35
    /**
36
     * @var CodeCoverage
37
     */
38
    private $coverage;
39
40
    /**
41
     * @var ConsoleIO
42
     */
43
    private $io;
44
45
    /**
46
     * @var array<string, mixed>
47
     */
48
    private $options;
49
50
    /**
51
     * @var array<string, mixed>
52
     */
53
    private $reports;
54
55
    /**
56
     * @var bool
57
     */
58
    private $skipCoverage;
59
60
    /**
61
     * CodeCoverageListener constructor.
62
     *
63
     * @param array<string, mixed> $reports
64
     */
65 5
    public function __construct(ConsoleIO $io, CodeCoverage $coverage, array $reports, bool $skipCoverage = false)
66
    {
67 5
        $this->io = $io;
68 5
        $this->coverage = $coverage;
69 5
        $this->reports = $reports;
70 5
        $this->options = [
71 5
            'whitelist' => ['src', 'lib'],
72 5
            'blacklist' => ['test', 'vendor', 'spec'],
73 5
            'whitelist_files' => [],
74 5
            'blacklist_files' => [],
75 5
            'output' => ['html' => 'coverage'],
76 5
            'format' => ['html'],
77 5
        ];
78
79 5
        $this->skipCoverage = $skipCoverage;
80
    }
81
82 13
    public function afterExample(ExampleEvent $event): void
83
    {
84 13
        if ($this->skipCoverage) {
85
            return;
86
        }
87
88 13
        $this->coverage->stop();
89
    }
90
91
    public function afterSuite(SuiteEvent $event): void
92
    {
93
        if ($this->skipCoverage) {
94
            if ($this->io->isVerbose()) {
95
                $this->io->writeln('Skipping code coverage generation');
96
            }
97
98
            return;
99
        }
100
101
        if ($this->io->isVerbose()) {
102
            $this->io->writeln();
103
        }
104
105
        foreach ($this->reports as $format => $report) {
106
            if ($this->io->isVerbose()) {
107
                $this->io->writeln(sprintf('Generating code coverage report in %s format ...', $format));
108
            }
109
110
            if ($report instanceof Report\Text) {
111
                $this->io->writeln(
112
                    $report->process($this->coverage, $this->io->isDecorated())
113
                );
114
            } else {
115
                $report->process($this->coverage, $this->options['output'][$format]);
116
            }
117
        }
118
    }
119
120
    public function beforeExample(ExampleEvent $event): void
121
    {
122
        if ($this->skipCoverage) {
123
            return;
124
        }
125
126
        $example = $event->getExample();
127
128
        $name = null;
129
130
        if (null !== $spec = $example->getSpecification()) {
131
            $name = $spec->getClassReflection()->getName();
132
        }
133
134
        $name = strtr('%spec%::%example%', [
135
            '%spec%' => $name,
136
            '%example%' => $example->getFunctionReflection()->getName(),
137
        ]);
138
139
        $this->coverage->start($name);
140
    }
141
142 4
    public function beforeSuite(SuiteEvent $event): void
143
    {
144 4
        if ($this->skipCoverage) {
145
            return;
146
        }
147
148 4
        $filter = $this->coverage->filter();
149
150
        // We compute the list of file / folder to be excluded
151
        // If the blacklist contains suffixes and/or prefixes, we extract an
152
        // exhaustive list of files that match to be added in the excluded list.
153 4
        $excludes = $this->options['blacklist_files'];
154 4
        foreach ($this->options['blacklist'] as $option) {
155 4
            $settings = $this->filterDirectoryParams($option);
156 4
            if (!empty($settings['suffix']) || !empty($settings['prefix'])) {
157 4
                $excludes = array_merge(
158 4
                    $excludes,
159 4
                    (new FileIteratorFacade())->getFilesAsArray(
160 4
                        $settings['directory'],
161 4
                        $settings['suffix'],
162 4
                        $settings['prefix']
163 4
                    )
164 4
                );
165
            } else {
166
                $excludes[] = $settings['directory'];
167
            }
168
        }
169 4
        $excludes = array_unique($excludes);
170
171 4
        foreach ($this->options['whitelist'] as $option) {
172 4
            $settings = $this->filterDirectoryParams($option);
173 2
            $fileIterator = (new FileIteratorFacade())->getFilesAsArray(
174 2
                [$settings['directory']] + $this->options['whitelist_files'],
175 2
                $settings['suffix'],
176 2
                $settings['prefix'],
177
                // We exclude the files from the previously built list.
178 2
                $excludes
179 2
            );
180
181 1
            foreach ($fileIterator as $file) {
182
                $filter->includeFile($file);
183
            }
184
        }
185
    }
186
187
    /**
188
     * @return array<string, array<int, int|string>>
189
     */
190
    public static function getSubscribedEvents(): array
191
    {
192
        return [
193
            'beforeExample' => ['beforeExample', -10],
194
            'afterExample' => ['afterExample', -10],
195
            'beforeSuite' => ['beforeSuite', -10],
196
            'afterSuite' => ['afterSuite', -10],
197
        ];
198
    }
199
200
    /**
201
     * @param array<string, mixed> $options
202
     */
203 4
    public function setOptions(array $options): void
204
    {
205 4
        $this->options = $options + $this->options;
206
    }
207
208
    /**
209
     * @param array<string, string>|string $option
210
     *
211
     * @return array{directory:non-empty-string, prefix:string, suffix:string}
0 ignored issues
show
Documentation Bug introduced by
The doc comment array{directory:non-empt...:string, suffix:string} at position 4 could not be parsed: Unknown type name 'non-empty-string' at position 4 in array{directory:non-empty-string, prefix:string, suffix:string}.
Loading history...
212
     */
213 4
    protected function filterDirectoryParams($option): array
214
    {
215 4
        if (is_string($option)) {
216 4
            $option = ['directory' => $option];
217
        }
218
219 4
        if (!is_array($option)) {
0 ignored issues
show
introduced by
The condition is_array($option) is always true.
Loading history...
220 1
            throw new ConfigurationException(sprintf(
221 1
                'Directory filtering options must be a string or an associated array, %s given instead.',
222 1
                gettype($option)
223 1
            ));
224
        }
225
226 4
        if (empty($option['directory'])) {
227 1
            throw new ConfigurationException('Missing required directory path.');
228
        }
229
230 4
        return [
231 4
            'directory' => $option['directory'],
232 4
            'suffix' => $option['suffix'] ?? '.php',
233 4
            'prefix' => $option['prefix'] ?? '',
234 4
        ];
235
    }
236
}
237