calculateCoverageThreshold()   F
last analyzed

Complexity

Conditions 38
Paths > 20000

Size

Total Lines 175
Code Lines 101

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 1482

Importance

Changes 0
Metric Value
eloc 101
dl 0
loc 175
ccs 0
cts 99
cp 0
rs 0
c 0
b 0
f 0
cc 38
nc 49803
nop 2
crap 1482

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
5
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
6
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
7
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
8
 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
9
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
10
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
11
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
12
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
13
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
14
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
15
 *
16
 * This software consists of voluntary contributions made by many individuals
17
 * and is licensed under the LGPL. For more information please see
18
 * <http://phing.info>.
19
 */
20
21
namespace Phing\Task\Ext\Coverage;
22
23
use Phing\Task;
24
use Phing\Type\Element\ClasspathAware;
25
use Phing\Exception\BuildException;
26
use Phing\Io\File;
27
use Phing\Type\Excludes;
28
use Phing\Util\Properties;
29
use Phing\Util\StringHelper;
30
use Phing\Task\Ext\PhpUnit\PHPUnitUtil;
31
32
/**
33
 * Stops the build if any of the specified coverage threshold was not reached
34
 *
35
 * @author  Benjamin Schultz <[email protected]>
36
 * @package phing.tasks.ext.coverage
37
 * @since   2.4.1
38
 */
39
class CoverageThresholdTask extends Task
40
{
41
    use ClasspathAware;
42
43
    /**
44
     * Holds the exclusions
45
     *
46
     * @var Excludes
47
     */
48
    private $excludes = null;
49
50
    /**
51
     * Holds an optional database file
52
     *
53
     * @var File
54
     */
55
    private $database = null;
56
57
    /**
58
     * Holds the coverage threshold for the entire project
59
     *
60
     * @var integer
61
     */
62
    private $perProject = 25;
63
64
    /**
65
     * Holds the coverage threshold for any class
66
     *
67
     * @var integer
68
     */
69
    private $perClass = 25;
70
71
    /**
72
     * Holds the coverage threshold for any method
73
     *
74
     * @var integer
75
     */
76
    private $perMethod = 25;
77
78
    /**
79
     * Holds the minimum found coverage value for a class
80
     *
81
     * @var integer
82
     */
83
    private $minClassCoverageFound = null;
84
85
    /**
86
     * Holds the minimum found coverage value for a method
87
     *
88
     * @var integer
89
     */
90
    private $minMethodCoverageFound = null;
91
92
    /**
93
     * Number of statements in the entire project
94
     *
95
     * @var integer
96
     */
97
    private $projectStatementCount = 0;
98
99
    /**
100
     * Number of covered statements in the entire project
101
     *
102
     * @var integer
103
     */
104
    private $projectStatementsCovered = 0;
105
106
    /**
107
     * Whether to enable detailed logging
108
     *
109
     * @var boolean
110
     */
111
    private $verbose = false;
112
113
    /**
114
     * Sets the optional coverage database to use
115
     *
116
     * @param File The database file
0 ignored issues
show
Bug introduced by
The type Phing\Task\Ext\Coverage\The was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
117
     */
118
    public function setDatabase(File $database)
119
    {
120
        $this->database = $database;
121
    }
122
123
    /**
124
     * Sets the coverage threshold for entire project
125
     *
126
     * @param integer $threshold Coverage threshold for entire project
127
     */
128
    public function setPerProject($threshold)
129
    {
130
        $this->perProject = $threshold;
131
    }
132
133
    /**
134
     * Sets the coverage threshold for any class
135
     *
136
     * @param integer $threshold Coverage threshold for any class
137
     */
138
    public function setPerClass($threshold)
139
    {
140
        $this->perClass = $threshold;
141
    }
142
143
    /**
144
     * Sets the coverage threshold for any method
145
     *
146
     * @param integer $threshold Coverage threshold for any method
147
     */
148
    public function setPerMethod($threshold)
149
    {
150
        $this->perMethod = $threshold;
151
    }
152
153
    /**
154
     * Sets whether to enable detailed logging or not
155
     *
156
     * @param boolean $verbose
157
     */
158
    public function setVerbose($verbose)
159
    {
160
        $this->verbose = StringHelper::booleanValue($verbose);
161
    }
162
163
    /**
164
     * Filter covered statements
165
     *
166
     * @param  integer $var Coverage CODE/count
167
     * @return boolean
168
     */
169
    protected function filterCovered($var)
170
    {
171
        return ($var >= 0 || $var === -2);
172
    }
173
174
    /**
175
     * Create excludes object
176
     *
177
     * @return Excludes
178
     */
179
    public function createExcludes()
180
    {
181
        $this->excludes = new Excludes($this->project);
182
183
        return $this->excludes;
184
    }
185
186
    /**
187
     * Calculates the coverage threshold
188
     *
189
     * @param  string $filename The filename to analyse
190
     * @param  array $coverageInformation Array with coverage information
191
     * @throws BuildException
192
     */
193
    protected function calculateCoverageThreshold($filename, $coverageInformation)
194
    {
195
        $classes = PHPUnitUtil::getDefinedClasses($filename, $this->classpath);
196
197
        if (is_array($classes)) {
0 ignored issues
show
introduced by
The condition is_array($classes) is always true.
Loading history...
198
            foreach ($classes as $className) {
199
                // Skip class if excluded from coverage threshold validation
200
                if ($this->excludes !== null) {
201
                    if (in_array($className, $this->excludes->getExcludedClasses())) {
202
                        continue;
203
                    }
204
                }
205
206
                $reflection = new \ReflectionClass($className);
207
                $classStartLine = $reflection->getStartLine();
208
209
                // Strange PHP5 reflection bug, classes without parent class
210
                // or implemented interfaces seem to start one line off
211
                if (
212
                    $reflection->getParentClass() === null
213
                    && count($reflection->getInterfaces()) === 0
214
                ) {
215
                    unset($coverageInformation[$classStartLine + 1]);
216
                } else {
217
                    unset($coverageInformation[$classStartLine]);
218
                }
219
220
                reset($coverageInformation);
221
222
                $methods = $reflection->getMethods();
223
224
                foreach ($methods as $method) {
225
                    // PHP5 reflection considers methods of a parent class
226
                    // to be part of a subclass, we don't
227
                    if ($method->getDeclaringClass()->getName() != $reflection->getName()) {
228
                        continue;
229
                    }
230
231
                    // Skip method if excluded from coverage threshold validation
232
                    if ($this->excludes !== null) {
233
                        $excludedMethods = $this->excludes->getExcludedMethods();
234
235
                        if (isset($excludedMethods[$className])) {
236
                            if (
237
                                in_array($method->getName(), $excludedMethods[$className])
238
                                || in_array($method->getName() . '()', $excludedMethods[$className])
239
                            ) {
240
                                continue;
241
                            }
242
                        }
243
                    }
244
245
                    $methodStartLine = $method->getStartLine();
246
                    $methodEndLine = $method->getEndLine();
247
248
                    // small fix for XDEBUG_CC_UNUSED
249
                    if (isset($coverageInformation[$methodStartLine])) {
250
                        unset($coverageInformation[$methodStartLine]);
251
                    }
252
253
                    if (isset($coverageInformation[$methodEndLine])) {
254
                        unset($coverageInformation[$methodEndLine]);
255
                    }
256
257
                    if ($method->isAbstract()) {
258
                        continue;
259
                    }
260
261
                    $lineNr = key($coverageInformation);
262
263
                    while ($lineNr !== null && $lineNr < $methodStartLine) {
264
                        next($coverageInformation);
265
                        $lineNr = key($coverageInformation);
266
                    }
267
268
                    $methodStatementsCovered = 0;
269
                    $methodStatementCount = 0;
270
271
                    while ($lineNr !== null && $lineNr <= $methodEndLine) {
272
                        $methodStatementCount++;
273
274
                        $lineCoverageInfo = $coverageInformation[$lineNr];
275
                        // set covered when CODE is other than -1 (not executed)
276
                        if ($lineCoverageInfo > 0 || $lineCoverageInfo === -2) {
277
                            $methodStatementsCovered++;
278
                        }
279
280
                        next($coverageInformation);
281
                        $lineNr = key($coverageInformation);
282
                    }
283
284
                    if ($methodStatementCount > 0) {
285
                        $methodCoverage = ($methodStatementsCovered
286
                                / $methodStatementCount) * 100;
287
                    } else {
288
                        $methodCoverage = 0;
289
                    }
290
291
                    if ($methodCoverage < $this->perMethod && !$method->isAbstract()) {
292
                        throw new BuildException(
293
                            'The coverage (' . round($methodCoverage, 2) . '%) '
294
                            . 'for method "' . $method->getName() . '" is lower'
295
                            . ' than the specified threshold ('
296
                            . $this->perMethod . '%), see file: "'
297
                            . $filename . '"'
298
                        );
299
                    }
300
301
                    if (
302
                        $methodCoverage < $this->perMethod
303
                        && $method->isAbstract()
304
                        && $this->verbose === true
305
                    ) {
306
                        $this->log(
307
                            'Skipped coverage threshold for abstract method "'
308
                            . $method->getName() . '"'
309
                        );
310
                    }
311
312
                    // store the minimum coverage value for logging (see #466)
313
                    if ($this->minMethodCoverageFound !== null) {
314
                        if ($this->minMethodCoverageFound > $methodCoverage) {
315
                            $this->minMethodCoverageFound = $methodCoverage;
316
                        }
317
                    } else {
318
                        $this->minMethodCoverageFound = $methodCoverage;
319
                    }
320
                }
321
322
                $classStatementCount = count($coverageInformation);
323
                $classStatementsCovered = count(
324
                    array_filter(
325
                        $coverageInformation,
326
                        [$this, 'filterCovered']
327
                    )
328
                );
329
330
                if ($classStatementCount > 0) {
331
                    $classCoverage = ($classStatementsCovered
332
                            / $classStatementCount) * 100;
333
                } else {
334
                    $classCoverage = 0;
335
                }
336
337
                if ($classCoverage < $this->perClass && !$reflection->isAbstract()) {
338
                    throw new BuildException(
339
                        'The coverage (' . round($classCoverage, 2) . '%) for class "'
340
                        . $reflection->getName() . '" is lower than the '
341
                        . 'specified threshold (' . $this->perClass . '%), '
342
                        . 'see file: "' . $filename . '"'
343
                    );
344
                }
345
346
                if (
347
                    $classCoverage < $this->perClass
348
                    && $reflection->isAbstract()
349
                    && $this->verbose === true
350
                ) {
351
                    $this->log(
352
                        'Skipped coverage threshold for abstract class "'
353
                        . $reflection->getName() . '"'
354
                    );
355
                }
356
357
                // store the minimum coverage value for logging (see #466)
358
                if ($this->minClassCoverageFound !== null) {
359
                    if ($this->minClassCoverageFound > $classCoverage) {
360
                        $this->minClassCoverageFound = $classCoverage;
361
                    }
362
                } else {
363
                    $this->minClassCoverageFound = $classCoverage;
364
                }
365
366
                $this->projectStatementCount += $classStatementCount;
367
                $this->projectStatementsCovered += $classStatementsCovered;
368
            }
369
        }
370
    }
371
372
    public function main()
373
    {
374
        if ($this->database === null) {
375
            $coverageDatabase = $this->project
376
                ->getProperty('coverage.database');
377
378
            if (!$coverageDatabase) {
379
                throw new BuildException(
380
                    'Either include coverage-setup in your build file or set '
381
                    . 'the "database" attribute'
382
                );
383
            }
384
385
            $database = new File($coverageDatabase);
386
        } else {
387
            $database = $this->database;
388
        }
389
390
        $this->log(
391
            'Calculating coverage threshold: min. '
392
            . $this->perProject . '% per project, '
393
            . $this->perClass . '% per class and '
394
            . $this->perMethod . '% per method is required'
395
        );
396
397
        $props = new Properties();
398
        $props->load($database);
399
400
        foreach ($props->keys() as $filename) {
401
            $file = unserialize($props->getProperty($filename));
0 ignored issues
show
Bug introduced by
It seems like $props->getProperty($filename) can also be of type null; however, parameter $data of unserialize() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

401
            $file = unserialize(/** @scrutinizer ignore-type */ $props->getProperty($filename));
Loading history...
402
403
            // Skip file if excluded from coverage threshold validation
404
            if ($this->excludes !== null) {
405
                if (in_array($file['fullname'], $this->excludes->getExcludedFiles())) {
406
                    continue;
407
                }
408
            }
409
410
            $this->calculateCoverageThreshold(
411
                $file['fullname'],
412
                $file['coverage']
413
            );
414
        }
415
416
        if ($this->projectStatementCount > 0) {
417
            $coverage = ($this->projectStatementsCovered
418
                    / $this->projectStatementCount) * 100;
419
        } else {
420
            $coverage = 0;
421
        }
422
423
        if ($coverage < $this->perProject) {
424
            throw new BuildException(
425
                'The coverage (' . round($coverage, 2) . '%) for the entire project '
426
                . 'is lower than the specified threshold ('
427
                . $this->perProject . '%)'
428
            );
429
        }
430
431
        $this->log(
432
            'Passed coverage threshold. Minimum found coverage values are: '
433
            . round($coverage, 2) . '% per project, '
434
            . round($this->minClassCoverageFound, 2) . '% per class and '
435
            . round($this->minMethodCoverageFound, 2) . '% per method'
436
        );
437
    }
438
}
439