Skip to content

Commit def28a8

Browse files
committed
Add additional clover_coverage task features
1 parent 5734d04 commit def28a8

9 files changed

+354
-29
lines changed

doc/tasks/clover_coverage.md

+25-3
Original file line numberDiff line numberDiff line change
@@ -13,17 +13,39 @@ grumphp:
1313
tasks:
1414
clover_coverage:
1515
clover_file: /tmp/clover.xml
16-
level: 100
16+
minimum_level: 100
17+
target_level: null
18+
merge_strategy: 'merge'
1719
```
1820
1921
**clover_file**
2022
2123
*Required*
2224
23-
The location of the clover code coverage XML file.
25+
The location of the clover code coverage XML file(s).
26+
When an array of files is provided, the results inside the clover files will be merged following the configured `merge_strategy`.
2427

25-
**level**
28+
**minimum_level**
2629

2730
*Default: 100*
2831

2932
The minimum code coverage percentage required to pass.
33+
34+
**target_level**
35+
36+
*Default: null*
37+
38+
Setting a minimum code coverage level is letting the task fail hard.
39+
When you are in the process of increasing your code coverage, you can set a target level.
40+
When the code coverage is below the target level, the task will fail in a non-blocking way.
41+
This gives you the opportunity to increase the code coverage step by step in a non-blocking way whilst keeping track of the progress.
42+
43+
**merge_strategy**
44+
45+
*Default: merge*
46+
47+
When an array of clover files is provided, the results will be merged following this strategy.
48+
Following strategies are available:
49+
50+
* `merge`: Can be used if the clover files cover the same sources. The total number of elements will be picked from the first provided file.
51+
* `combine`: Can be used if the clover files cover different sources. The total number of elements will be added to each other.

doc/tasks/phpunit.md

+30
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,10 @@ grumphp:
2323
exclude_group: []
2424
always_execute: false
2525
order: null
26+
coverage-clover: null
27+
coverage-html: null
28+
coverage-php: null
29+
coverage-xml: null
2630
```
2731
2832
**config_file**
@@ -69,3 +73,29 @@ Always run the whole test suite, even if no PHP files were changed.
6973
*Default: null*
7074

7175
If you wish to run tests in a specific order. `order: [default,defects,duration,no-depends,random,reverse,size]`
76+
77+
78+
**coverage-clover**
79+
80+
*Default: null*
81+
82+
Generate code coverage report in Clover XML format.
83+
84+
**coverage-html**
85+
86+
*Default: null*
87+
88+
Generate code coverage report in HTML format.
89+
90+
**coverage-php**
91+
92+
*Default: null*
93+
94+
Serialize PHP_CodeCoverage object to file.
95+
96+
**coverage-xml**
97+
98+
*Default: null*
99+
100+
Generate code coverage report in PHPUnit XML format.
101+

src/Task/CloverCoverage.php

+103-17
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace GrumPHP\Task;
66

7+
use GrumPHP\Exception\FileNotFoundException;
78
use GrumPHP\Runner\TaskResult;
89
use GrumPHP\Runner\TaskResultInterface;
910
use GrumPHP\Task\Config\ConfigOptionsResolver;
@@ -15,10 +16,15 @@
1516
use GrumPHP\Util\Filesystem;
1617
use SimpleXMLElement;
1718
use SplFileInfo;
19+
use Symfony\Component\OptionsResolver\Options;
1820
use Symfony\Component\OptionsResolver\OptionsResolver;
21+
use VeeWee\Xml\Dom\Document;
1922

2023
class CloverCoverage implements TaskInterface
2124
{
25+
public const MERGE_STRATEGY_COMBINE = 'combine';
26+
public const MERGE_STRATEGY_MERGE = 'merge';
27+
2228
/**
2329
* @var Filesystem
2430
*/
@@ -53,16 +59,41 @@ public static function getConfigurableOptions(): ConfigOptionsResolver
5359
$resolver = new OptionsResolver();
5460

5561
$resolver->setDefined('clover_file');
56-
$resolver->setDefined('level');
62+
$resolver->setDefined('minimum_level');
63+
$resolver->setDefined('target_level');
64+
$resolver->setDefined('merge_strategy');
5765

58-
$resolver->addAllowedTypes('clover_file', ['string']);
59-
$resolver->addAllowedTypes('level', ['int', 'float']);
66+
$resolver->setRequired('clover_file');
67+
68+
$resolver->addAllowedTypes('clover_file', ['string', 'string[]']);
69+
$resolver->addAllowedTypes('minimum_level', ['int', 'float']);
70+
$resolver->addAllowedTypes('target_level', ['int', 'float', 'null']);
71+
72+
$resolver->addAllowedTypes('merge_strategy', ['string']);
73+
$resolver->setAllowedValues('merge_strategy', [
74+
self::MERGE_STRATEGY_COMBINE,
75+
self::MERGE_STRATEGY_MERGE,
76+
]);
6077

6178
$resolver->setDefaults([
62-
'level' => 100,
79+
'minimum_level' => 100,
80+
'target_level' => null,
81+
'merge_strategy' => self::MERGE_STRATEGY_MERGE,
6382
]);
6483

65-
$resolver->setRequired('clover_file');
84+
// @deprecated : Can be removed on 3.0.0
85+
$resolver->setDefined('level');
86+
$resolver->setDeprecated(
87+
'level',
88+
'grumphp',
89+
'2.8.0',
90+
'The level has been deprecated and will be removed in 3.0.0. Use minimum_level instead.'
91+
);
92+
$resolver->addAllowedTypes('level', ['int', 'float']);
93+
$resolver->setDefault('minimum_level', function (Options $options): int|float {
94+
return (float) ($options['level'] ?? 100);
95+
});
96+
// @deprecated : end
6697

6798
return ConfigOptionsResolver::fromOptionsResolver($resolver);
6899
}
@@ -81,41 +112,96 @@ public function canRunInContext(ContextInterface $context): bool
81112
public function run(ContextInterface $context): TaskResultInterface
82113
{
83114
$configuration = $this->getConfig()->getOptions();
84-
$percentage = round(min(100, max(0, (float) $configuration['level'])), 2);
85-
$cloverFile = (string) $configuration['clover_file'];
86-
87-
if (!$this->filesystem->exists($cloverFile)) {
88-
return TaskResult::createFailed($this, $context, 'Invalid input file provided');
115+
$clamp = static fn (float $value): float => round(min(100, max(0, $value)), 2);
116+
$minimumLevel = $clamp((float) $configuration['minimum_level']);
117+
$targetLevel = $configuration['target_level'] ? $clamp((float) $configuration['target_level']) : null;
118+
/** @var list<string> $cloverFiles */
119+
$cloverFiles = (array) $configuration['clover_file'];
120+
/** @var CloverCoverage::MERGE_STRATEGY_* $mergeStrategy */
121+
$mergeStrategy = (string) $configuration['merge_strategy'];
122+
123+
if (!count($cloverFiles)) {
124+
return TaskResult::createFailed($this, $context, 'No clover file(s) provided');
89125
}
90126

91-
if (!$percentage) {
127+
if ($minimumLevel === 0.0) {
92128
return TaskResult::createFailed(
93129
$this,
94130
$context,
95-
'An integer checked percentage must be given as second parameter'
131+
'You must provide a positive minimum level between 1-100 for code coverage.'
96132
);
97133
}
98134

99-
$xml = new SimpleXMLElement($this->filesystem->readFromFileInfo(new SplFileInfo($cloverFile)));
100-
$totalElements = (int) current($xml->xpath('/coverage/project/metrics/@elements') ?? []);
101-
$checkedElements = (int) current($xml->xpath('/coverage/project/metrics/@coveredelements') ?? []);
135+
try {
136+
[
137+
'totalElements' => $totalElements,
138+
'checkedElements' => $checkedElements
139+
] = $this->parseTotals($cloverFiles, $mergeStrategy);
140+
} catch (FileNotFoundException $exception) {
141+
return TaskResult::createFailed($this, $context, $exception->getMessage());
142+
}
102143

103144
if (0 === $totalElements) {
104145
return TaskResult::createSkipped($this, $context);
105146
}
106147

107148
$coverage = round(($checkedElements / $totalElements) * 100, 2);
108149

109-
if ($coverage < $percentage) {
150+
if ($coverage < $minimumLevel) {
110151
$message = sprintf(
111152
'Code coverage is %1$d%%, which is below the accepted %2$d%%'.PHP_EOL,
112153
$coverage,
113-
$percentage
154+
$minimumLevel
114155
);
115156

116157
return TaskResult::createFailed($this, $context, $message);
117158
}
118159

160+
if ($targetLevel !== null && $coverage < $targetLevel) {
161+
$message = sprintf(
162+
'Code coverage is %1$d%%, which is below the target %2$d%%'.PHP_EOL,
163+
$coverage,
164+
$targetLevel
165+
);
166+
167+
return TaskResult::createNonBlockingFailed($this, $context, $message);
168+
}
169+
119170
return TaskResult::createPassed($this, $context);
120171
}
172+
173+
/**
174+
* @param list<string> $coverageFiles
175+
* @param CloverCoverage::MERGE_STRATEGY_* $mergeStrategy
176+
* @return array{'totalElements': int, 'checkedElements': int}
177+
*
178+
* @throws FileNotFoundException
179+
*/
180+
private function parseTotals(array $coverageFiles, string $mergeStrategy): array
181+
{
182+
$result = [
183+
'totalElements' => 0,
184+
'checkedElements' => 0,
185+
];
186+
187+
foreach ($coverageFiles as $file) {
188+
if (!$this->filesystem->exists($file)) {
189+
throw new FileNotFoundException($file);
190+
}
191+
192+
$xml = new SimpleXMLElement($this->filesystem->readFromFileInfo(new SplFileInfo($file)));
193+
$totalElements = (int) current($xml->xpath('/coverage/project/metrics/@elements') ?? []);
194+
$checkedElements = (int) current($xml->xpath('/coverage/project/metrics/@coveredelements') ?? []);
195+
196+
$result = [
197+
'totalElements' => match ($mergeStrategy) {
198+
self::MERGE_STRATEGY_COMBINE => $result['totalElements'] + $totalElements,
199+
self::MERGE_STRATEGY_MERGE => $result['totalElements'] ?: $totalElements,
200+
},
201+
'checkedElements' => $result['checkedElements'] + $checkedElements
202+
];
203+
}
204+
205+
return $result;
206+
}
121207
}

src/Task/Phpunit.php

+12
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ public static function getConfigurableOptions(): ConfigOptionsResolver
2828
'exclude_group' => [],
2929
'always_execute' => false,
3030
'order' => null,
31+
'coverage-clover' => null,
32+
'coverage-html' => null,
33+
'coverage-php' => null,
34+
'coverage-xml' => null,
3135
]);
3236

3337
$resolver->addAllowedTypes('config_file', ['null', 'string']);
@@ -36,6 +40,10 @@ public static function getConfigurableOptions(): ConfigOptionsResolver
3640
$resolver->addAllowedTypes('exclude_group', ['array']);
3741
$resolver->addAllowedTypes('always_execute', ['bool']);
3842
$resolver->addAllowedTypes('order', ['null', 'string']);
43+
$resolver->addAllowedTypes('coverage-clover', ['null', 'string']);
44+
$resolver->addAllowedTypes('coverage-html', ['null', 'string']);
45+
$resolver->addAllowedTypes('coverage-php', ['null', 'string']);
46+
$resolver->addAllowedTypes('coverage-xml', ['null', 'string']);
3947

4048
return ConfigOptionsResolver::fromOptionsResolver($resolver);
4149
}
@@ -60,6 +68,10 @@ public function run(ContextInterface $context): TaskResultInterface
6068
$arguments->addOptionalCommaSeparatedArgument('--group=%s', $config['group']);
6169
$arguments->addOptionalCommaSeparatedArgument('--exclude-group=%s', $config['exclude_group']);
6270
$arguments->addOptionalArgument('--order-by=%s', $config['order']);
71+
$arguments->addOptionalArgument('--coverage-clover=%s', $config['coverage-clover']);
72+
$arguments->addOptionalArgument('--coverage-html=%s', $config['coverage-html']);
73+
$arguments->addOptionalArgument('--coverage-php=%s', $config['coverage-php']);
74+
$arguments->addOptionalArgument('--coverage-xml=%s', $config['coverage-xml']);
6375

6476
$process = $this->processBuilder->buildProcess($arguments);
6577
$process->run();

src/Test/Task/AbstractTaskTestCase.php

+3-2
Original file line numberDiff line numberDiff line change
@@ -95,15 +95,16 @@ public function it_fails_on_stuff(
9595
ContextInterface $context,
9696
callable $configurator,
9797
string $expectedErrorMessage,
98-
string $resultClass = TaskResult::class
98+
string $resultClass = TaskResult::class,
99+
int $resultCode = TaskResult::FAILED,
99100
): void {
100101
$task = $this->configureTask($config);
101102
\Closure::bind($configurator, $this)($task->getConfig()->getOptions(), $context);
102103

103104
$result = $task->run($context);
104105

105106
self::assertInstanceOf($resultClass, $result);
106-
self::assertSame(TaskResult::FAILED, $result->getResultCode());
107+
self::assertSame($resultCode, $result->getResultCode());
107108
self::assertSame($task, $result->getTask());
108109
self::assertSame($context, $result->getContext());
109110

0 commit comments

Comments
 (0)