Passed
Branch master (e44da5)
by Eric
02:16
created

CoverageCheckCommand   A

Complexity

Total Complexity 16

Size/Duplication

Total Lines 191
Duplicated Lines 0 %

Test Coverage

Coverage 100%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 82
c 1
b 0
f 0
dl 0
loc 191
ccs 94
cts 94
cp 1
rs 10
wmc 16
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\Command;
17
18
use Esi\CoverageCheck\Application;
19
use Esi\CoverageCheck\CoverageCheck;
20
use Esi\CoverageCheck\Style\CoverageCheckStyle;
21
use Esi\CoverageCheck\Utils;
22
use Override;
23
use Symfony\Component\Console\Attribute\AsCommand;
24
use Symfony\Component\Console\Command\Command;
25
use Symfony\Component\Console\Helper\TableCell;
26
use Symfony\Component\Console\Helper\TableCellStyle;
27
use Symfony\Component\Console\Helper\TableSeparator;
28
use Symfony\Component\Console\Input\InputArgument;
29
use Symfony\Component\Console\Input\InputInterface;
30
use Symfony\Component\Console\Input\InputOption;
31
use Symfony\Component\Console\Output\OutputInterface;
32
use Throwable;
33
34
/**
35
 * @see \Esi\CoverageCheck\Tests\Command\CoverageCheckCommandTest
36
 *
37
 * @psalm-suppress PropertyNotSetInConstructor
38
 */
39
#[AsCommand(name: Application::COMMAND_NAME, description: Application::APPLICATION_DESCRIPTION)]
40
final class CoverageCheckCommand extends Command
41
{
42
    /**
43
     * Matches CoverageCheck::ERROR_INSUFFICIENT_DATA, except for '[ERROR]' prefix.
44
     *
45
     * @see CoverageCheck::ERROR_INSUFFICIENT_DATA
46
     *
47
     * @internal
48
     *
49
     * @since 3.0.0
50
     */
51
    public const string ERROR_INSUFFICIENT_DATA = 'Insufficient data for calculation. Please add more code.';
1 ignored issue
show
Bug introduced by
A parse error occurred: Syntax error, unexpected T_STRING, expecting '=' on line 51 at column 24
Loading history...
52
53
    /**
54
     * Matches CoverageCheck::ERROR_COVERAGE_BELOW_THRESHOLD, except for '[ERROR]' prefix and '%' on first value.
55
     *
56
     * @see CoverageCheck::ERROR_COVERAGE_BELOW_THRESHOLD
57
     *
58
     * @internal
59
     *
60
     * @since 3.0.0
61
     */
62
    public const string ERROR_COVERAGE_BELOW_THRESHOLD = 'Total code coverage is %s which is below the accepted %d%%';
63
64
    /**
65
     * Matches CoverageCheck::OK_TOTAL_CODE_COVERAGE, except for '[OK]' prefix and '%' on value.
66
     *
67
     * @see CoverageCheck::OK_TOTAL_CODE_COVERAGE
68
     *
69
     * @internal
70
     *
71
     * @since 3.0.0
72
     */
73
    public const string OK_TOTAL_CODE_COVERAGE = 'Total code coverage is %s';
74
75
    /**
76
     * @internal
77
     *
78
     * @since 3.0.0
79
     */
80
    private const string INPUT_ARGUMENT_CLOVERFILE = 'The location of the clover xml file that is generated by PHPUnit.';
81
82
    /**
83
     * @internal
84
     *
85
     * @since 3.0.0
86
     */
87
    private const string INPUT_ARGUMENT_THRESHOLD = 'The coverage threshold that is acceptable. Min = 1, Max = 100';
88
89
    /**
90
     * @internal
91
     *
92
     * @since 3.0.0
93
     */
94
    private const string INPUT_OPTION_ONLY_PERCENTAGE = 'Only return the resulting coverage percentage';
95
96
    /**
97
     * @internal
98
     *
99
     * @since 3.0.0
100
     */
101
    private const string INPUT_OPTION_SHOW_FILES = 'Show a breakdown of coverage by file';
102
103
    private CoverageCheckStyle $coverageCheckStyle;
104
105 15
    public function __construct(private readonly CoverageCheck $coverageCheck)
106
    {
107 15
        parent::__construct();
108
    }
109
110
    /**
111
     * @see Command
112
     */
113 15
    #[Override]
114
    protected function configure(): void
115
    {
116 15
        $this
117 15
            ->setDefinition([
118 15
                new InputArgument('cloverfile', InputArgument::REQUIRED, self::INPUT_ARGUMENT_CLOVERFILE),
119 15
                new InputArgument('threshold', InputArgument::REQUIRED, self::INPUT_ARGUMENT_THRESHOLD),
120 15
                new InputOption('--only-percentage', '-O', InputOption::VALUE_NONE, self::INPUT_OPTION_ONLY_PERCENTAGE),
121 15
                new InputOption('--show-files', '-F', InputOption::VALUE_NONE, self::INPUT_OPTION_SHOW_FILES),
122 15
            ])
123 15
            ->setHelp(
124 15
                <<<'EOF'
125
                    The <info>%command.name%</info> command calculates coverage score for the provided clover xml report.
126
127
                    You must also pass a coverage threshold that is acceptable. <info>Min = 1, Max = 100</info>:
128
129
                    <info>php %command.full_name% /path/to/clover.xml 100</info>
130
131
                    You may also choose to only return the resulting coverage percentage by using the <info>--only-percentage</info> option:
132
133
                    <info>php %command.full_name% /path/to/clover.xml 100 --only-percentage</info>
134
135
                    You may also choose to show a breakdown of coverage by file by using the <info>--show-files</info> option:
136
137
                    <info>php %command.full_name% /path/to/clover.xml 100 --show-files</info>
138 15
                    EOF
139 15
            )
140 15
        ;
141
    }
142
143
    /**
144
     * @see Command
145
     * @see CoverageCheck
146
     * @see CoverageCheckStyle
147
     *
148
     * Suppress for Psalm as atm the var annotations are still needed for PHPStan.
149
     *
150
     * @psalm-suppress UnnecessaryVarAnnotation
151
     */
152 15
    #[Override]
153
    protected function execute(InputInterface $input, OutputInterface $output): int
154
    {
155 15
        $this->coverageCheckStyle = new CoverageCheckStyle($input, $output);
156
157
        /** @phpstan-var string $cloverFile */
158 15
        $cloverFile = $input->getArgument('cloverfile');
159
160
        /** @phpstan-var string $threshold */
161 15
        $threshold = $input->getArgument('threshold');
162
163
        /** @phpstan-var bool $onlyPercentage */
164 15
        $onlyPercentage = $input->getOption('only-percentage');
165
166
        /** @phpstan-var bool $showFiles */
167 15
        $showFiles = $input->getOption('show-files');
168
169 15
        $this->coverageCheck->setCloverFile($cloverFile)
170 15
            ->setThreshold((int) $threshold)
171 15
            ->setOnlyPercentage($onlyPercentage);
172
173
        try {
174 12
            $result = $showFiles ? $this->coverageCheck->processByFile() : $this->coverageCheck->process();
175 3
        } catch (Throwable $throwable) {
176 3
            $this->coverageCheckStyle->error($throwable->getMessage());
177
178 3
            return Command::INVALID;
179
        }
180
181
        // No metrics
182 9
        if ($result === false) {
183 3
            $this->coverageCheckStyle->error(self::ERROR_INSUFFICIENT_DATA);
184
185 3
            return Command::FAILURE;
186
        }
187
188
        // --show-files
189 6
        if (\is_array($result)) {
190 2
            return $this->getFileTable($result);
191
        }
192
193
        // Standard output
194 4
        return $this->getResultOutput($result);
195
    }
196
197
    /**
198
     * @param array{
199
     *     fileMetrics: array<string, array{coveredMetrics: int, totalMetrics: int, percentage: float|int}>,
200
     *     totalCoverage: float|int
201
     * } $result
202
     */
203 2
    private function getFileTable(array $result): int
204
    {
205 2
        $threshold     = $this->coverageCheck->getThreshold();
206 2
        $tableRows     = [];
207 2
        $totalElements = ['coveredMetrics' => 0, 'totalMetrics' => 0];
208 2
        $metrics       = $result['fileMetrics'];
209 2
        $totalCoverage = $result['totalCoverage'];
210
211 2
        unset($result);
212
213 2
        foreach ($metrics as $name => $file) {
214 2
            $tableRows[] = [
215 2
                $name,
216 2
                \sprintf('%d/%d', $file['coveredMetrics'], $file['totalMetrics']),
217 2
                new TableCell(
218 2
                    Utils::formatCoverage($file['percentage']),
219 2
                    [
220 2
                        'style' => new TableCellStyle(
221 2
                            [
222 2
                                'cellFormat' => ($file['percentage'] < $threshold) ? '<error>%s</error>' : '<info>%s</info>',
223 2
                            ]
224 2
                        ),
225 2
                    ]
226 2
                ),
227 2
            ];
228
229 2
            $totalElements['coveredMetrics'] += $file['coveredMetrics'];
230 2
            $totalElements['totalMetrics']   += $file['totalMetrics'];
231
        }
232
233 2
        unset($metrics);
234
235 2
        $tableRows[] = new TableSeparator();
236 2
        $tableRows[] = [
237 2
            'Overall Totals',
238 2
            \sprintf('%d/%d', $totalElements['coveredMetrics'], $totalElements['totalMetrics']),
239 2
            new TableCell(
240 2
                Utils::formatCoverage($totalCoverage),
241 2
                ['style' => new TableCellStyle(['cellFormat' => ($totalCoverage < $threshold) ? '<error>%s</error>' : '<info>%s</info>', ])]
242 2
            ),
243 2
        ];
244
245 2
        $this->coverageCheckStyle->table(
246 2
            ['File', 'Elements (Covered/Total)', 'Coverage'],
247 2
            $tableRows
248 2
        );
249
250 2
        unset($tableRows);
251
252 2
        if ($totalCoverage < $threshold) {
253 1
            return Command::FAILURE;
254
        }
255
256 1
        return Command::SUCCESS;
257
    }
258
259 4
    private function getResultOutput(float $result): int
260
    {
261 4
        $threshold         = $this->coverageCheck->getThreshold();
262 4
        $onlyPercentage    = $this->coverageCheck->getOnlyPercentage();
263 4
        $formattedCoverage = Utils::formatCoverage($result);
264 4
        $belowThreshold    = $result < $threshold;
265
266
        // Only display the percentage?
267 4
        if ($onlyPercentage) {
268
            // ... below the accepted threshold
269 2
            if ($belowThreshold) {
270 1
                $this->coverageCheckStyle->error($formattedCoverage, true);
271
272 1
                return Command::FAILURE;
273
            }
274
275
            // all good, we meet or exceed the threshold
276 1
            $this->coverageCheckStyle->success($formattedCoverage, true);
277
278 1
            return Command::SUCCESS;
279
        }
280
281
        // We want the full message…
282 2
        if ($belowThreshold) {
283
            // ... below the accepted threshold
284 1
            $this->coverageCheckStyle->error(
285 1
                \sprintf(self::ERROR_COVERAGE_BELOW_THRESHOLD, $formattedCoverage, $threshold)
286 1
            );
287
288 1
            return Command::FAILURE;
289
        }
290
291
        // all good, we meet or exceed the threshold
292 1
        $this->coverageCheckStyle->success(\sprintf(self::OK_TOTAL_CODE_COVERAGE, $formattedCoverage));
293
294 1
        return Command::SUCCESS;
295
    }
296
}
297