Passed
Push — master ( 63b8d4...042808 )
by Eric
02:00
created

CoverageCheck::processByFile()   A

Complexity

Conditions 5
Paths 7

Size

Total Lines 52
Code Lines 27

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 27
CRAP Score 5

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 5
eloc 27
c 3
b 0
f 0
nc 7
nop 0
dl 0
loc 52
rs 9.1768
ccs 27
cts 27
cp 1
crap 5

How to fix   Long Method   

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
declare(strict_types=1);
4
5
/**
6
 * This file is part of PHPUnit Coverage Check.
7
 *
8
 * (c) Eric Sizemore <[email protected]>
9
 * (c) Richard Regeer <[email protected]>
10
 *
11
 * This source file is subject to the MIT license. For the full copyright,
12
 * license information, and credits/acknowledgements, please view the LICENSE
13
 * and README files that were distributed with this source code.
14
 */
15
16
namespace Esi\CoverageCheck;
17
18
use InvalidArgumentException;
19
use RuntimeException;
20
use SimpleXMLElement;
21
22
use function array_map;
23
use function file_get_contents;
24
25
/**
26
 * @see Command\CoverageCheckCommand
27
 * @see Tests\CoverageCheckTest
28
 */
29
class CoverageCheck
30
{
31
    /**
32
     * Xpath expression for getting each file's data in a clover report.
33
     *
34
     * @since 2.0.0
35
     */
36
    protected const XPATH_FILES = '//file';
37
38
    /**
39
     * Xpath expression for getting the project total metrics in a clover report.
40
     */
41
    protected const XPATH_METRICS = '//project/metrics';
42
43
    /**
44
     * Configurable options.
45
     *
46
     * @see self::setCloverFile()
47
     * @see self::setThreshold()
48
     * @see self::setOnlyPercentage()
49
     */
50
    protected string $cloverFile = 'clover.xml';
51
52
    protected bool $onlyPercentage = false;
53
54
    protected int $threshold = 100;
55
56
    /**
57
     * Simple getters.
58
     */
59
60 1
    public function getCloverFile(): string
61
    {
62 1
        return $this->cloverFile;
63
    }
64
65 5
    public function getOnlyPercentage(): bool
66
    {
67 5
        return $this->onlyPercentage;
68
    }
69
70 7
    public function getThreshold(): int
71
    {
72 7
        return $this->threshold;
73
    }
74
75
    /**
76
     * Processes the coverage data with the given clover file and threshold, and returns the information
77
     * similar to how the Console application will.
78
     *
79
     * This is mainly useful for those that may wish to use this library outside the CLI/Console or PHAR.
80
     *
81
     * @throws InvalidArgumentException If the clover file does not exist, or the threshold is not within
82
     *                                  defined range (>= 1 <= 100).
83
     */
84 8
    public function nonConsoleCall(string $cloverFile, int $threshold = 100, bool $onlyPercentage = false): string
85
    {
86 8
        $this->setCloverFile($cloverFile)
87 8
            ->setThreshold($threshold)
88 8
            ->setOnlyPercentage($onlyPercentage);
89
90 8
        $results = $this->process();
91
92 5
        if ($results === false) {
93 1
            return '[ERROR] Insufficient data for calculation. Please add more code.';
94
        }
95
96 4
        if ($results < $threshold && !$onlyPercentage) {
97 1
            return \sprintf(
98 1
                '[ERROR] Total code coverage is %s which is below the accepted %d%%',
99 1
                Utils::formatCoverage($results),
100 1
                $threshold
101 1
            );
102
        }
103
104 3
        if ($onlyPercentage) {
105 2
            return Utils::formatCoverage($results);
106
        }
107
108 1
        return \sprintf('[OK] Total code coverage is %s', Utils::formatCoverage($results));
109
    }
110
111
    /**
112
     * Parses the clover xml file for coverage metrics.
113
     *
114
     * According to Atlassian:
115
     *     TPC = (coveredconditionals + coveredstatements + coveredmethods) / (conditionals + statements + methods)
116
     *
117
     * Though it appears elements + coveredelements should work the same, I am sticking with Atlassian's
118
     * calculation.
119
     *
120
     * @see self::loadMetrics()
121
     * @see https://confluence.atlassian.com/pages/viewpage.action?pageId=79986990
122
     * @see https://ocramius.github.io/blog/automated-code-coverage-check-for-github-pull-requests-with-travis/
123
     */
124 17
    public function process(): false|float
125
    {
126 17
        $rawMetrics = $this->loadMetrics() ?? false;
127
128
        // Ignoring coverage here as theoretically this should not happen
129
        //@codeCoverageIgnoreStart
130
        if ($rawMetrics === false) {
131
            return false;
132
        }
133
134
        //@codeCoverageIgnoreEnd
135
136
        /**
137
         * @var array<string> $metrics
138
         */
139 11
        $metrics = ((array) $rawMetrics[0])['@attributes'];
140
141 11
        $metrics = array_map(static fn (string $value): int => \intval($value), $metrics);
142
143 11
        unset($rawMetrics);
144
145 11
        $coveredMetrics = $metrics['coveredconditionals'] + $metrics['coveredstatements'] + $metrics['coveredmethods'];
146 11
        $totalMetrics   = $metrics['conditionals'] + $metrics['statements'] + $metrics['methods'];
147
148 11
        unset($metrics);
149
150 11
        if ($totalMetrics === 0) {
151 3
            return false;
152
        }
153
154 8
        return $coveredMetrics / $totalMetrics * 100;
155
    }
156
157
    /**
158
     * Parses the clover xml file for coverage metrics by file.
159
     *
160
     * @see self::process()
161
     * @see self::loadMetrics()
162
     * @see https://confluence.atlassian.com/pages/viewpage.action?pageId=79986990
163
     * @see https://ocramius.github.io/blog/automated-code-coverage-check-for-github-pull-requests-with-travis/
164
     * @since 2.0.0
165
     *
166
     * @return false|array{
167
     *     fileMetrics: array<string, array{coveredMetrics: int, totalMetrics: int, percentage: float|int}>,
168
     *     totalCoverage: float|int
169
     * }
170
     */
171 3
    public function processByFile(): array|false
172
    {
173 3
        $fileMetrics          = [];
174 3
        $totalElementsCovered = 0;
175 3
        $totalElements        = 0;
176
177 3
        $rawMetrics = $this->loadMetrics(self::XPATH_FILES) ?? false;
178
179
        // Ignoring coverage here as theoretically this should not happen
180
        //@codeCoverageIgnoreStart
181
        if ($rawMetrics === false) {
182
            return false;
183
        }
184
        //@codeCoverageIgnoreEnd
185
186 3
        foreach ($rawMetrics as $file) {
187
            /**
188
             * @var array<string> $metrics
189
             */
190 3
            $metrics = ((array) $file->metrics)['@attributes'];
191
192 3
            $metrics = array_map(static fn (string $value): int => \intval($value), $metrics);
193
194 3
            $coveredMetrics = ($metrics['coveredconditionals'] + $metrics['coveredstatements'] + $metrics['coveredmethods']);
195 3
            $totalMetrics   = ($metrics['conditionals'] + $metrics['statements'] + $metrics['methods']);
196
197 3
            if ($totalMetrics === 0) {
198 1
                continue;
199
            }
200
201 2
            $coveragePercentage = $coveredMetrics / $totalMetrics * 100;
202 2
            $totalElementsCovered += $coveredMetrics;
203 2
            $totalElements        += $totalMetrics;
204
205 2
            $fileMetrics[(string) $file['name']] = [
206 2
                'coveredMetrics' => $coveredMetrics,
207 2
                'totalMetrics'   => $totalMetrics,
208 2
                'percentage'     => $coveragePercentage,
209 2
            ];
210
        }
211
212 3
        unset($rawMetrics);
213
214 3
        if ($totalElements === 0) {
215 1
            return false;
216
        }
217
218 2
        $totalCoverage = $totalElementsCovered / $totalElements * 100;
219
220 2
        return [
221 2
            'fileMetrics'   => $fileMetrics,
222 2
            'totalCoverage' => $totalCoverage,
223 2
        ];
224
    }
225
226
    /**
227
     * Simple setters.
228
     */
229
230
    /**
231
     * @throws InvalidArgumentException If the given file is empty or does not exist.
232
     */
233 25
    public function setCloverFile(string $cloverFile): CoverageCheck
234
    {
235 25
        if (!Utils::validateCloverFile($cloverFile)) {
236 2
            throw new InvalidArgumentException(\sprintf('Invalid input file provided. Was given: %s', $cloverFile));
237
        }
238
239 23
        $this->cloverFile = $cloverFile;
240
241 23
        return $this;
242
    }
243
244 21
    public function setOnlyPercentage(bool $enable = false): CoverageCheck
245
    {
246 21
        $this->onlyPercentage = $enable;
247
248 21
        return $this;
249
    }
250
251
    /**
252
     * @throws InvalidArgumentException If the threshold is less than 1 or greater than 100.
253
     */
254 24
    public function setThreshold(int $threshold): CoverageCheck
255
    {
256 24
        if (!Utils::validateThreshold($threshold)) {
257 3
            throw new InvalidArgumentException(\sprintf('The threshold must be a minimum of 1 and a maximum of 100, %d given', $threshold));
258
        }
259
260 21
        $this->threshold = $threshold;
261
262 21
        return $this;
263
    }
264
265
    /**
266
     * Loads the clover xml data and runs an XML Xpath query.
267
     *
268
     * @internal
269
     *
270
     * @param self::XPATH_* $xpath
271
     *
272
     * @throws RuntimeException If file_get_contents fails or if XML data cannot be parsed, or
273
     *                          if the given file does not appear to be a valid clover file.
274
     *
275
     * @return null|array<SimpleXMLElement>|false
276
     */
277 20
    protected function loadMetrics(string $xpath = self::XPATH_METRICS): null|array|false
278
    {
279 20
        $cloverData = file_get_contents($this->cloverFile);
280
281
        //@codeCoverageIgnoreStart
282
        if ($cloverData === false || $cloverData === '') {
283
            throw new RuntimeException(\sprintf('Failed to get the contents of %s', $this->cloverFile));
284
        }
285
        //@codeCoverageIgnoreEnd
286
287 20
        $xml = Utils::parseXml($cloverData);
288
289 20
        if (!Utils::isPossiblyClover($xml)) {
290 6
            throw new RuntimeException('Clover file appears to be invalid. Are you sure this is a PHPUnit generated clover report?');
291
        }
292
293 14
        return $xml->xpath($xpath);
294
    }
295
}
296