Passed
Push — master ( 92d243...5a69a1 )
by Eric
12:07
created

CoverageCheck::nonConsoleCall()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 16
CRAP Score 5

Importance

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